2.3 Declarações de controle e funções
Nesta seção nós vamos falar sobre declarações de controle e operações de funções em Go.
Declarações de Controle
A maior invenção na programação é o controle de fluxo. Por causa dele, você é capaz de utilizar declarações de controle simples que podem ser usadas para representar lógicas complexas. Existem três categorias de controle de fluxo: condicional, controle de ciclo e salto incondicional.
if
if
provavelmente será a palavra-chave mais utilizada nos seus programas. Se ele atende as condições, então ele faz alguma coisa e caso contrário faz alguma outra coisa.
if
não precisa de parênteses em Go.
if x > 10 {
fmt.Println("x is greater than 10")
} else {
fmt.Println("x is less than or equal to 10")
}
A coisa mais útil sobre o if
em Go é que ele pode ter uma instrução de inicialização antes da instrução condicional. O escopo das variáveis definidas nesta instrução de inicialização só estão disponíveis dentro do bloco de definição do if
.
// inicializa x e então confere se x é maior que 10
if x := computedValue(); x > 10 {
fmt.Println("x is greater than 10")
} else {
fmt.Println("x is less than 10")
}
// o código seguinte não compilará
fmt.Println(x)
Use if-else para múltiplas condições.
if integer == 3 {
fmt.Println("The integer is equal to 3")
} else if integer < 3 {
fmt.Println("The integer is less than 3")
} else {
fmt.Println("The integer is greater than 3")
}
goto
Go possui uma palavra-chave chamada goto
, mas cuidado ao utilizar ela. goto
reencaminha o fluxo de controle para um label
definido anteriormente dentro do mesmo bloco de código.
func myFunc() {
i := 0
Here: // label termina com ":"
fmt.Println(i)
i++
goto Here // pule para o label "Here"
}
O nome do label é case sensitive.
for
for
é a lógica de controle mais poderosa em Go. Ele pode ler dados em loops e operações iterativas, assim como o while
.
for expression1; expression2; expression3 {
//...
}
expression1
, expression2
e expression3
são todas expressões, onde expression1
e expression3
são definições de variáveis ou atribuições, e expression2
é uma declaração condicional. expression1
será executada uma vez antes do loop, e expression3
será executada depois de cada iteração do loop.
Exemplos são mais úteis que palavras.
package main
import "fmt"
func main(){
sum := 0;
for index:=0; index < 10 ; index++ {
sum += index
}
fmt.Println("sum is equal to ", sum)
}
// Mostra: sum is equal to 45
Algumas vezes nós precisamos de várias atribuições, porém Go não possui o operador ,
, então nós usamos atribuições paralelas como i, j = i + 1, j - 1
.
Nós podemos omitir as expressões expression1
e expression3
se elas não forem necessárias.
sum := 1
for ; sum < 1000; {
sum += sum
}
Podemos omitir também o ;
. Isto lhe parece familiar? Sim, é idêntico ao while
.
sum := 1
for sum < 1000 {
sum += sum
}
Existem duas operações importantes em loops que são break
e continue
. break
"pula" fora do loop, e continue
ignora o loop atual e inicia o próximo. Se você tiver loops aninhados use break
juntamente com labels.
for index := 10; index>0; index-- {
if index == 5{
break // ou continue
}
fmt.Println(index)
}
// break mostra 10、9、8、7、6
// continue mostra 10、9、8、7、6、4、3、2、1
for
pode ler dados de um slice
ou map
quando for utilizado junto com range
.
for k,v:=range map {
fmt.Println("map's key:",k)
fmt.Println("map's val:",v)
}
Como Go suporta múltiplos valores de retorno e gera um erro de compilação caso você não use os valores que foram definidos, você pode querer utilizar _
para descartar certos valores de retorno.
for _, v := range map{
fmt.Println("map's val:", v)
}
switch
As vezes você pode achar que está usando muitas instruções if-else
para implementar uma lógica, o que pode dificultar a leitura e manutenção no futuro. Este é o momento perfeito para utilizar a instrução switch
para resolver este problema.
switch sExpr {
case expr1:
some instructions
case expr2:
some other instructions
case expr3:
some other instructions
default:
other code
}
Os tipos de sExpr
, expr1
, expr2
, e expr3
devem ser o mesmo. switch
é muito flexível. As condições não precisam ser constantes e são executadas de cima para baixo até que encontre uma condição válida. Se não houver nenhuma instrução após a palavra-chave switch
, então ela corresponderá a true
.
i := 10
switch i {
case 1:
fmt.Println("i is equal to 1")
case 2, 3, 4:
fmt.Println("i is equal to 2, 3 or 4")
case 10:
fmt.Println("i is equal to 10")
default:
fmt.Println("All I know is that i is an integer")
}
Na quinta linha, colocamos vários valores em um case
, e não precisamos adicionar a palavra-chave break
no final do bloco do case
. Ele irá sair do bloco do switch quando encontrar uma condição verdadeira. Se você deseja continuar verificando mais casos, você precisará utilizar a instrução fallthrough
.
integer := 6
switch integer {
case 4:
fmt.Println("integer <= 4")
fallthrough
case 5:
fmt.Println("integer <= 5")
fallthrough
case 6:
fmt.Println("integer <= 6")
fallthrough
case 7:
fmt.Println("integer <= 7")
fallthrough
case 8:
fmt.Println("integer <= 8")
fallthrough
default:
fmt.Println("default case")
}
Este programa mostra a seguinte informação.
integer <= 6
integer <= 7
integer <= 8
default case
Funções
Use a palavra-chave func
para definir uma função.
func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
// corpo da função
// retorno de múltiplos valores
return value1, value2
}
Nós podemos extrapolar as seguintes informações do exemplo acima.
- Use a palavra-chave
func
para definir uma funçãofuncName
. - Funções tem zero, um ou mais argumentos. O tipo do argumento vem depois do nome do argumento e vários argumentos são separados por
,
. - Funções podem retornar múltiplos valores.
- Existem dois valores de retorno com os nomes
output1
eoutput2
, você pode omitir estes nomes e usar apenas os tipos deles. - Se existe apenas um valor de retorno e você omitir o nome, você não precisa usar colchetes nos valores de retorno.
- Se a função não possui valores de retorno, você pode omitir os parâmetros de retorno completamente.
- Se a função possui valores de retorno, você precisa utilizar a instrução
return
em algum lugar no corpo da função.
Vamos ver um exemplo prático. (calcular o valor máximo)
package main
import "fmt"
// retorna o maior valor entre a e b
func max(a, b int) int {
if a > b {
return a
}
return b
}
func main() {
x := 3
y := 4
z := 5
max_xy := max(x, y) // chama a função max(x, y)
max_xz := max(x, z) // chama a função max(x, z)
fmt.Printf("max(%d, %d) = %d\n", x, y, max_xy)
fmt.Printf("max(%d, %d) = %d\n", x, z, max_xz)
fmt.Printf("max(%d, %d) = %d\n", y, z, max(y,z)) // chama a função max aqui
}
No exemplo acima existem dois argumentos do tipo int
na função max
, sendo assim o tipo do primeiro argumento pode ser omitido. Por exemplo, pode-se utilizar a, b int
em vez de a int, b int
. As mesmas regras se aplicam para argumentos adicionais. Observe aqui que max
só tem um valor de retorno, então nós só precisamos escrever o tipo do valor de retorno. Esta é a forma curta de escrevê-lo.
Múltiplos valores de retorno
Uma coisa em que Go é melhor que C é que ela suporta múltiplos valores de retorno.
Usaremos o seguinte exemplo.
package main
import "fmt"
// Retorna os resultados de A + B e A * B
func SumAndProduct(A, B int) (int, int) {
return A+B, A*B
}
func main() {
x := 3
y := 4
xPLUSy, xTIMESy := SumAndProduct(x, y)
fmt.Printf("%d + %d = %d\n", x, y, xPLUSy)
fmt.Printf("%d * %d = %d\n", x, y, xTIMESy)
}
O exemplo acima retorna dois valores sem nomes - você também tem a opção de nomeá-los se preferir. Se nomearmos os valores de retorno, poderemos utilizar apenas a instrução return
para retornar os valores já que eles foram inicializados na função automaticamente. Observe que se suas funções forem utilizadas fora do pacote, o que significa que o nome das funções começam com uma letra maiúscula, é melhor escrever instruções completas para o return
. Isto torna o seu código mais legível.
func SumAndProduct(A, B int) (add int, Multiplied int) {
add = A+B
Multiplied = A*B
return
}
Argumentos variáveis
Go suporta argumentos variáveis, o que significa que você pode dar um número incerto de argumentos para funções.
func myfunc(arg ...int) {}
A instrução arg …int
significa que esta é uma função que possui argumentos variáveis. Observe que estes argumentos são do tipo int
. No corpo da função o arg
se torna um slice
de int
.
for _, n := range arg {
fmt.Printf("And the number is: %d\n", n)
}
Passar por valor e ponteiros
Quando passamos um argumento para uma função que foi chamada, a função recebe na verdade uma cópia da nossa variáveis original, sendo assim, as alterações não afetarão a variável original.
Vejamos um exemplo para provar o que eu estou dizendo.
package main
import "fmt"
// Função simples que adiciona 1 a variável a
func add1(a int) int {
a = a+1 // alteramos o valor de a
return a // retornamos o novo valor de a
}
func main() {
x := 3
fmt.Println("x = ", x) // deve mostrar "x = 3"
x1 := add1(x) // chama add1(x)
fmt.Println("x+1 = ", x1) // deve mostrar "x+1 = 4"
fmt.Println("x = ", x) // deve mostrar "x = 3"
}
Você consegue ver isso? Mesmo que nós chamamos add1
com x
, o valor original de x
não é alterado.
A razão é muito simples: quando chamamos add1
, nós passamos uma cópia de x
para a função, e não o próprio x
.
Agora você pode se perguntar, como eu posso passar o x
original para a função.
Precisamos usar ponteiros aqui. Sabemos que as variáveis são armazenadas em memória e que elas possuem endereços de memória. Então, se queremos alterar o valor de uma variável, precisamos alterar o valor armazenado no endereço de memória. Portanto, a função add1
precisa saber o endereço de memória de x
para poder alterar o seu valor. Para isto, passamos &x
para a função, e alteramos o tipo do argumento para o tipo ponteiro *int
. Esteja ciente de que nós passamos uma cópia do ponteiro, não uma cópia do valor.
package main
import "fmt"
// Função simples que adiciona 1 a variável a
func add1(a *int) int {
*a = *a+1 // alteramos o valor de a
return *a // retornamos o novo valor de a
}
func main() {
x := 3
fmt.Println("x = ", x) // deve mostrar "x = 3"
x1 := add1(&x) // call add1(&x) pass memory address of x
fmt.Println("x+1 = ", x1) // deve mostrar "x+1 = 4"
fmt.Println("x = ", x) // deve mostrar "x = 4"
}
Agora podemos alterar o valor de x
dentro da função. Por que usamos ponteiros? Quais são as vantagens?
- Permite-nos usar mais funções para operar em uma variável.
- Baixo custo passando endereços de memória (8 bytes), a cópia não é uma maneira eficiente, tanto em termo de tempo como de espaço, para passar variáveis.
string
,slice
,map
são tipos de referências, sendo assim, por padrão eles utilizam ponteiros ao passar para uma função. (Atenção: Se você precisa alterar o comprimento de umslice
, você precisa passar ponteiros explicitamente)
defer
Go possui uma palavra-chave bem projetada chamada defer
. Você pode ter muitas declarações defer
em uma função. Elas serão executadas em ordem inversa quando o programa executa até o final das funções. No caso onde o programa abre alguns arquivos de recurso, estes arquivos precisam ser fechados antes que a função possa retornar com erros. Vamos ver alguns exemplos.
func ReadWrite() bool {
file.Open("file")
// Faça alguma tarefa
if failureX {
file.Close()
return false
}
if failureY {
file.Close()
return false
}
file.Close()
return true
}
Vimos algum código sendo repetido várias vezes. defer
resolve este problema muito bem. Ela não só ajuda você a escrever um código limpo, como também torna seu código mais legível.
func ReadWrite() bool {
file.Open("file")
defer file.Close()
if failureX {
return false
}
if failureY {
return false
}
return true
}
Se houver mais de uma instrução defer
, elas serão executadas em ordem inversa. O exemplo a seguir irá mostrar 4 3 2 1 0
.
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
Funções como tipos e valores
Funções também são variáveis em Go, podemos utilizar type
para defini-las. Funções que possuem a mesma assinatura podem ser vistas como sendo do mesmo tipo.
type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])
Qual é a vantagem deste recurso? A resposta é que isto nos permite passar funções como valores.
package main
import "fmt"
type testInt func(int) bool // define o tipo de uma função como variável
func isOdd(integer int) bool {
if integer%2 == 0 {
return false
}
return true
}
func isEven(integer int) bool {
if integer%2 == 0 {
return true
}
return false
}
// passa a função `f` como um argumento para outra função
func filter(slice []int, f testInt) []int {
var result []int
for _, value := range slice {
if f(value) {
result = append(result, value)
}
}
return result
}
func main(){
slice := []int {1, 2, 3, 4, 5, 7}
fmt.Println("slice = ", slice)
odd := filter(slice, isOdd) // usa a função como valor
fmt.Println("Odd elements of slice are: ", odd)
even := filter(slice, isEven)
fmt.Println("Even elements of slice are: ", even)
}
Isto é muito útil quando utilizamos interfaces. Como você pode ver testInt
é uma variável que tem tipo de função, e os valores de retorno e argumentos de filter
são os mesmos de testInt
. Portanto, podemos ter lógica complexa em nossos programas mantendo a flexibilidade em nosso código.
Panic e Recover
Go não possui a estrutura try-catch
assim como Java. Em vez de lançar exceções, Go usa panic
e recover
para lidar com erros. No entanto, embora poderoso, você não deve utilizar a instrução panic
muito.
Panic é uma função interna para quebrar o fluxo normal de programas e entrar em estado de pânico. Quando uma função F
chama panic
, F
não continuará executando, mas suas funções defer
continuarão a ser executadas. Então F
volta ao ponto de interrupção que causou o estado de pânico. O programa não terminará até que todas essas funções retornem com panic para o primeiro nível da goroutine
. panic
pode ser gerado executando a instrução panic
no programa, e alguns erros também causam panic
, como, por exemplo, a tentativa de acessar uma posição inválida em um array.
Recover é uma função interna utilizada para recuperar goroutines de estados de pânico. Chamar recover
nas funções defer
é útil porque as funções normais não serão executadas quando o programa estiver em estado de pânico. Ele recebe os valores de panic
se o programa está em estado de pânico, e recebe nil
se o programa não está em estado de pânico.
O seguinte exemplo mostra como utilizar panic
.
var user = os.Getenv("USER")
func init() {
if user == "" {
panic("no value for $USER")
}
}
O seguinte exemplo mostra como verificar panic
.
func throwsPanic(f func()) (b bool) {
defer func() {
if x := recover(); x != nil {
b = true
}
}()
f() // se f causar pânico, ele irá recuperar
return
}
Função main
e função init
Go possui duas retenções que são chamadas de main
e init
, onde init
pode ser usada em todos os pacotes e main
só pode ser usada no pacote main
. Estas duas funções não são capazes de ter argumentos ou valores de retorno. Mesmo que possamos escrever muitas funções init
em um pacote, recomendo fortemente escrever apenas uma função init
para cada pacote.
Programas em Go irão chamar as funções init()
e main()
automaticamente, então você não precisa se preocupar em chamá-las. Para cada pacote, a função init
é opcional, mas o package main
tem uma e apenas uma função main
.
Programas inicializam e começam a execução a partir do pacote main
. Se o pacote main
importa outros pacotes, eles serão importados em tempo de compilação. Se um pacote é importado muitas vezes, ele será compilado apenas uma vez. Depois de importar pacotes, os programas irão inicializar as constantes e variáveis dentro dos pacotes importados, e então executar a função init
se ela existir, e assim por diante. Depois de todos os outros pacotes serem inicializados, os programas irão inicializar as constantes e variáveis do pacote main
e então executar a função init
dentro do pacote, se ela existir. A figura a seguir mostra o processo.
Figure 2.6 Fluxo de inicialização de programas em Go
import
Usamos import
muito frequentemente em programas Go da seguinte forma.
import(
"fmt"
)
Então, usamos funções deste pacote da seguinte maneira.
fmt.Println("hello world")
fmt
é da biblioteca padrão Go, que está localizada em $GOROOT/pkg. Go suporta pacotes de terceiros de duas maneiras.
- Caminho relativo import "./model" // carrega o pacote no mesmo diretório, eu não recomendo utilizar esta forma.
- Caminho absoluto import "shorturl/model" // carrega o pacote no caminho "$GOPATH/pkg/shorturl/model"
Existem alguns operadores especiais quando importamos pacotes, e iniciantes na linguagem sempre se confundem com estes operadores.
Operador: ponto. Às vezes vemos pessoas usando a seguinte forma para importar pacotes.
import( . "fmt" )
O operador ponto significa que você pode omitir o nome do pacote quando você chamar funções dentro deste pacote. Assim sendo,
fmt.Printf("Hello world")
torna-sePrintf("Hello world")
.Operador: pseudônimo (alias) Ele altera o nome do pacote que importamos quando chamamos funções que pertencem a este pacote.
import( f "fmt" )
Assim sendo,
fmt.Printf("Hello world")
torna-sef.Printf("Hello world")
.Operador:
_
. Este operador é difícil de entender sem alguém explicando para você.import ( "database/sql" _ "github.com/ziutek/mymysql/godrv" )
O operador
_
na verdade significa que só queremos importar este pacote e executar sua funçãoinit
, e não temos certeza se queremos usar as funções pertencentes a este pacote.
Links
- Sumário
- Seção anterior: Fundamentos em Go
- Próxima seção: struct