A palavra-chave else executa um bloco quando a condição do if anterior é falsa. Escreve else if para testar outra condição e encadeia quantos precisares. O else tem de ficar na mesma linha que a chaveta de fecho do bloco if, caso contrário o código não compila. O Go idiomático usa o else menos do que a maioria das linguagens. Quando o bloco if termina em return, o código a seguir ao if já é o caso else, por isso o código Go retorna cedo e deixa o caminho feliz sem indentação (especificação do Go).
Resumo rápido
if cond { ... } else { ... }precisa de chavetas à volta dos dois blocos e não leva parênteses à volta da condição.} else {tem de ficar numa só linha. Uma quebra de linha depois da}termina a instruçãoif, e o compilador reportasyntax error: unexpected keyword else, expected }.- As cadeias
else ifsão avaliadas de cima para baixo e ganha a primeira condição verdadeira. As cadeias longas leem-se melhor como umswitchsem expressão. - Uma variável declarada na instrução curta do
if(if v, err := f(); err != nil) é visível em todos os ramoselse ifeelsee desaparece depois da}final. - Quando o bloco
ifretorna, elimina oelse. O linterreviveassinala a forma que sobra comif block ends with a return statement, so drop this else and outdent its block. - O Go não tem o operador
?:. Usaif/else, oucmp.Orquando só precisas de um valor por omissão para um valor zero.
Como se escreve um if/else em Go?
Um if recebe uma condição booleana e um bloco. Um bloco else a seguir executa quando a condição é falsa. Eis a decisão que um ciclo de retentativas toma depois de um pedido falhado:
example.gogopackage main import "fmt" func main() { attempt := 3 maxAttempts := 5 if attempt < maxAttempts { fmt.Printf("attempt %d failed, retrying\n", attempt) } else { fmt.Printf("attempt %d failed, giving up\n", attempt) } }
example.texttextattempt 3 failed, retrying
Há três regras diferentes de C, Java e JavaScript:
- Sem parênteses à volta da condição.
if (attempt < maxAttempts) {compila, mas ogofmtremove os parênteses. - As chavetas são obrigatórias, mesmo para um corpo de uma só linha.
if code >= 500 fmt.Println("server error")falha comsyntax error: unexpected name fmt, expected {. - A condição tem de ser um
bool. O Go não tem valores truthy, por issoif len(items)eif usersão erros de tipo. Escreveif len(items) > 0eif user != nil.
A mesma regra das chavetas aplica-se ao else. Uma instrução solta depois do else falha com syntax error: else must be followed by if or statement block. Só outro if ou um bloco { ... } o podem seguir. A palavra-chave if em Go aborda as condições e as instruções de inicialização do lado do if.
Porque é que o else tem de ficar na mesma linha que a }?
A gramática do Go usa pontos e vírgulas para terminar instruções, mas quase nunca os escreves. O lexer insere um no fim de uma linha quando o último token é um identificador, um literal, uma das palavras-chave break, continue, fallthrough ou return, um operador como ++, ou um ), ] ou } de fecho (especificação do Go, Semicolons).
É por causa desta regra que isto falha:
example.gogoif retries > 0 { fmt.Println("retrying") } else { fmt.Println("giving up") }
example.texttext./main.go:10:2: syntax error: unexpected keyword else, expected }
A linha que termina em } recebe um ponto e vírgula, que termina a instrução if. A linha seguinte começa então uma nova instrução com else, e nenhuma instrução pode começar com else. A correção é pôr } else { numa só linha, que é também o que o gofmt produz. A mesma regra explica porque é que o Go põe a chaveta de abertura na mesma linha que o próprio if.
Os programadores de C e Java que preferem o estilo Allman (chaveta numa linha própria) deparam-se com isto logo na primeira semana. A vantagem é que todas as bases de código Go formatam o if/else da mesma maneira, por isso o estilo nunca é tema de code review.
Como funciona o else if em Go?
O else if não é uma palavra-chave à parte. É um else seguido de outra instrução if, algo que a gramática permite diretamente: "else" ( IfStmt | Block ). Encadeia quantos precisares. O Go avalia-os de cima para baixo e executa o primeiro ramo cuja condição é verdadeira.
Este ciclo distribui as latências dos pedidos por buckets de métricas:
example.gogopackage main import ( "fmt" "time" ) func main() { latencies := []time.Duration{ 40 * time.Millisecond, 250 * time.Millisecond, 3 * time.Second, 90 * time.Millisecond, } var fast, slow, timedOut int for _, d := range latencies { if d < 100*time.Millisecond { fast++ } else if d < time.Second { slow++ } else { timedOut++ } } fmt.Printf("fast=%d slow=%d timed_out=%d\n", fast, slow, timedOut) }
example.texttextfast=2 slow=1 timed_out=1
A ordem importa. Troca as duas primeiras condições para que d < time.Second venha primeiro e o resultado passa a ser fast=0 slow=3 timed_out=1. Todos os pedidos abaixo de 100ms também estão abaixo de um segundo, por isso a verificação mais larga apanha-os primeiro e o ramo fast nunca é executado. Põe o intervalo mais estreito primeiro.
Quando uma cadeia passa dos três ramos, um switch sem expressão costuma ler-se melhor. Cada case é uma condição, os cases são avaliados pela mesma ordem de cima para baixo e o default substitui o else final:
example.gogofor _, d := range latencies { switch { case d < 100*time.Millisecond: fast++ case d < time.Second: slow++ default: timedOut++ } }
O resultado é o mesmo. Vê a secção sobre switch do guia das palavras-chave do Go para perceber em que é que o switch do Go difere do de C. O switch e o select usam default para o caso «nada mais correspondeu», por isso o else só aparece depois de um if.
Qual é o âmbito de uma variável declarada numa instrução if?
Um if pode começar com uma instrução curta, normalmente uma declaração :=. As variáveis que ela declara têm como âmbito toda a instrução if, o que inclui todos os ramos else if e else. A especificação coloca cada if no seu próprio bloco implícito, e os ramos else ficam dentro dele (especificação do Go, Blocks).
É aqui que o else é difícil de substituir. Verificar um valor PORT lido do ambiente precisa do número convertido em todos os ramos:
example.gogopackage main import ( "fmt" "strconv" ) func main() { for _, raw := range []string{"8080", "80", "http"} { if port, err := strconv.Atoi(raw); err != nil { fmt.Printf("PORT=%q is not a number: %v\n", raw, err) } else if port < 1024 { fmt.Printf("PORT=%d is privileged (below 1024)\n", port) } else { fmt.Printf("PORT=%d ok\n", port) } } }
example.texttextPORT=8080 ok PORT=80 is privileged (below 1024) PORT="http" is not a number: strconv.Atoi: parsing "http": invalid syntax
port e err estão disponíveis nos três ramos. Depois da chaveta de fecho deixam de existir. Acrescenta fmt.Println("listening on", port) depois do if e o build falha com undefined: port. Se precisares do valor depois, declara-o numa linha própria antes do if.
A armadilha do shadowing
Como a instrução curta declara variáveis novas, pode esconder uma variável exterior com o mesmo nome. Isto compila e passa no go vet:
example.gogovar cfg Config if cfg, err := loadConfig(data); err != nil { log.Fatal(err) } else { fmt.Printf("loaded config, addr=%q\n", cfg.Addr) } fmt.Printf("starting server, addr=%q\n", cfg.Addr)
example.texttextloaded config, addr=":8443" starting server, addr=""
O cfg dentro do if é uma variável nova. O cfg exterior fica com o seu valor zero, e o servidor arranca com um endereço vazio. Escreve cfg, err := loadConfig(data) numa linha própria e verifica o err com um if simples. O artigo sobre a palavra-chave var em Go aborda o shadowing e o analisador shadow que o deteta.
Porque é que o Go idiomático evita o else?
A maioria das funções Go é uma série de passos em que cada um pode falhar. Se cada passo ficar aninhado dentro do else do anterior, o caminho de sucesso desliza para a direita e o tratamento de erros acaba longe da verificação que o causou. Um handler de encomendas escrito dessa forma:
example.gogofunc handleCreateOrder(w http.ResponseWriter, r *http.Request) { var req CreateOrderRequest if err := json.NewDecoder(r.Body).Decode(&req); err == nil { if req.Quantity > 0 { if id, err := saveOrder(req); err == nil { w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(map[string]string{"id": id}) } else { http.Error(w, "could not save order", http.StatusInternalServerError) } } else { http.Error(w, "quantity must be positive", http.StatusBadRequest) } } else { http.Error(w, "invalid JSON body", http.StatusBadRequest) } }
Para ver o que acontece com JSON inválido, tens de ler até ao fim da função. Inverte cada condição para que o caso de erro venha primeiro e retorne:
example.gogofunc handleCreateOrder(w http.ResponseWriter, r *http.Request) { var req CreateOrderRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid JSON body", http.StatusBadRequest) return } if req.Quantity <= 0 { http.Error(w, "quantity must be positive", http.StatusBadRequest) return } id, err := saveOrder(req) if err != nil { http.Error(w, "could not save order", http.StatusInternalServerError) return } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(map[string]string{"id": id}) }
Uma execução com httptest das duas versões contra quatro corpos de pedido (JSON partido, quantidade zero, um SKU sem stock e uma encomenda válida) dá códigos de estado e corpos idênticos. A segunda versão não tem nenhum else. Cada verificação fica junto à sua resposta de erro, e as duas últimas linhas são o caminho de sucesso encostado à margem esquerda. Acrescentar um quarto passo de validação significa acrescentar mais um bloco if, não mais um nível de aninhamento.
A equipa do Go descreve este estilo no Effective Go: «Como os casos de erro tendem a terminar em instruções return, o código resultante não precisa de instruções else.» A página Go Code Review Comments chama-lhe Indent Error Flow: «Tenta manter o caminho normal do código com a indentação mínima, e indenta o tratamento de erros, lidando com ele primeiro.»
O que dizem os linters sobre o else
O linter revive (o sucessor mantido do golint) ativa duas regras sobre o else por omissão. Ao executá-lo sobre este código:
example.gogofunc readTimeout() (int, error) { secs, err := strconv.Atoi(os.Getenv("TIMEOUT_SECONDS")) if err != nil { return 0, fmt.Errorf("parse TIMEOUT_SECONDS: %w", err) } else { return secs, nil } } func countValid(lines []string) int { n := 0 for _, line := range lines { if line == "" { continue } else { n++ } } return n }
reporta:
example.texttextlint.go:13:9: if block ends with a return statement, so drop this else and outdent its block lint.go:23:10: if block ends with a continue statement, so drop this else and outdent its block
A primeira vem da indent-error-flow, que dispara quando o bloco if termina em return. A segunda vem da superfluous-else, que cobre continue, break e goto e as chamadas que nunca retornam, como panic, os.Exit e log.Fatal. O revive tem também uma regra opcional, a early-return, para a forma invertida, em que é o bloco else que retorna. O golangci-lint expõe as mesmas regras através do seu linter revive.
Nenhuma das regras por omissão assinala o handler aninhado acima, porque nenhum dos seus blocos retorna. O linter só apanha o caso mecânico, e achatar uma pirâmide de verificações de sucesso é um refactor que tens de fazer tu.
Quando é que o else é a escolha certa em Go?
Eliminar o else é uma regra para blocos que saem. Quando os dois ramos fazem trabalho real e nenhum deles sai da função, o else é a forma mais clara de dizer «um destes dois». Isso acontece em três situações.
A primeira é quando os dois resultados continuam. Uma consulta à cache conta um hit ou um miss e depois continua, e como nenhum ramo retorna, não há nada para desindentar:
example.gogoif entry, ok := cache[key]; ok { hits++ resp = entry } else { misses++ resp = fetchFromOrigin(key) cache[key] = resp }
A segunda é quando escolhes entre dois valores. Para um valor barato, define o valor por omissão e substitui-o:
example.gogolevel := slog.LevelInfo if debug { level = slog.LevelDebug }
Quando construir o valor por omissão tem um custo ou um efeito secundário, como abrir uma ligação, usa if/else para que só um ramo seja executado:
example.gogovar store Store if redisURL != "" { store = RedisStore{url: redisURL} } else { store = MemoryStore{} }
Definir o valor por omissão e depois substituí-lo significaria var store Store = MemoryStore{} seguido de uma substituição. Isso só funciona aqui porque MemoryStore{} não custa nada. Se o valor por omissão abrisse um ficheiro ou um pool de ligações, a substituição deitaria fora um recurso que nunca usaste.
A terceira é quando há uma variável da instrução curta. Quando todos os ramos precisam dela, como no exemplo do PORT, o else mantém-na com o âmbito limitado à verificação. Declará-la antes do if também funciona, mas assim ela fica no âmbito durante o resto da função.
Existe um operador ternário em Go?
Não. O Go não tem cond ? a : b. A Go FAQ explica porquê: «A razão pela qual o ?: não existe em Go é que os criadores da linguagem tinham visto a operação ser usada com demasiada frequência para criar expressões impenetravelmente complexas. A forma if-else, embora mais longa, é inquestionavelmente mais clara. Uma linguagem só precisa de uma construção condicional de controlo de fluxo.»
A alternativa é o if/else ou a forma de valor por omissão com substituição da secção anterior. Para o caso comum de «usa este valor a não ser que esteja vazio», o Go 1.22 acrescentou o cmp.Or, que devolve o primeiro dos seus argumentos que não é o valor zero:
example.gogopackage main import ( "cmp" "fmt" "os" ) func main() { addr := cmp.Or(os.Getenv("LISTEN_ADDR"), ":8080") fmt.Println("listening on", addr) }
example.texttextlistening on :8080
O cmp.Or é uma função, não um operador, por isso o Go avalia todos os argumentos antes da chamada. cmp.Or(cachedToken, fetchToken()) chama fetchToken sempre, mesmo quando cachedToken está definido. Um helper genérico ternary(cond, a, b) tem o mesmo problema, o que é uma das razões pelas quais as bases de código Go raramente definem um. Quando um ramo tem um custo, escreve o if.
Onde entra o LevelUpGo
O LevelUpGo ensina Go através de exercícios que executam código Go real no navegador. O Go Basics aborda o if, o else if, o switch e o retorno antecipado em caso de erro, como parte de aprender a linguagem do zero. O Simplification tem lições sobre achatar código aninhado, simplificar lógica booleana e substituir cadeias else if por switch. Para as outras 24 palavras reservadas, vê Palavras-chave do Go: as 25 explicadas.
Perguntas frequentes
O que faz o else em Go?
O else executa um bloco quando a condição do if anterior é falsa. Pode ser seguido de um bloco (else { ... }) ou de outro if (else if cond { ... }). É uma das 25 palavras-chave reservadas do Go e só pode aparecer depois de um bloco if.
Porque é que o Go dá "syntax error: unexpected keyword else"?
O else está numa linha nova depois da } de fecho do bloco if. O Go insere um ponto e vírgula depois de uma } no fim de uma linha, o que termina a instrução if, por isso o else da linha seguinte fica sem nada a que se ligar. Escreve } else { numa só linha, ou corre o gofmt.
O Go tem elif ou elseif?
Não. O Go escreve-o em duas palavras, else if, que é um else seguido de uma nova instrução if. Para cadeias longas, um switch sem expressão a seguir à palavra-chave lê-se de forma mais limpa.
Devo usar else depois de return em Go?
Não. Se o bloco if termina com return, o código a seguir ao if só é executado quando a condição era falsa, por isso o else não acrescenta nada além de indentação. O revive assinala-o com a regra indent-error-flow, e o Go Code Review Comments recomenda tratar o erro primeiro e manter o caminho normal sem indentação.
Posso usar uma variável da instrução if no bloco else?
Sim. Uma variável declarada na instrução curta, como em if n, err := strconv.Atoi(s); err != nil, está no âmbito do bloco if e de todos os ramos else if e else. Sai do âmbito depois da última chaveta de fecho.
Como escrevo um if/else numa só linha em Go?
Não em código formatado. Um if/else numa só linha compila, mas o gofmt divide-o em várias linhas, e o Go não tem operador ternário. Usa um if/else normal, atribui um valor por omissão e substitui-o num if, ou usa o cmp.Or quando precisas de uma alternativa para um valor zero.
Fontes
- The Go Programming Language Specification, If statements: https://go.dev/ref/spec#If_statements
- The Go Programming Language Specification, Semicolons: https://go.dev/ref/spec#Semicolons
- The Go Programming Language Specification, Blocks: https://go.dev/ref/spec#Blocks
- Effective Go, If: https://go.dev/doc/effective_go#if
- Go Code Review Comments, Indent Error Flow: https://go.dev/wiki/CodeReviewComments#indent-error-flow
- Go FAQ, Does Go have the ?: operator?: https://go.dev/doc/faq#Does_Go_have_a_ternary_form
- revive rules (indent-error-flow, superfluous-else, early-return): https://github.com/revive-lint/revive/blob/master/RULES_DESCRIPTIONS.md
- cmp package, Or: https://pkg.go.dev/cmp#Or
