Voltar ao blog

A palavra-chave else em Go: if/else, else if, retornos antecipados e âmbito

Como funciona a palavra-chave else do Go: sintaxe if/else, porque é que o else tem de partilhar a linha com a chaveta de fecho, cadeias else if, âmbito das instruções if, retornos antecipados e a ausência do operador ternário.

A palavra-chave else em Go: if/else, else if, retornos antecipados e âmbito

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ção if, e o compilador reporta syntax error: unexpected keyword else, expected }.
  • As cadeias else if são avaliadas de cima para baixo e ganha a primeira condição verdadeira. As cadeias longas leem-se melhor como um switch sem expressão.
  • Uma variável declarada na instrução curta do if (if v, err := f(); err != nil) é visível em todos os ramos else if e else e desaparece depois da } final.
  • Quando o bloco if retorna, elimina o else. O linter revive assinala a forma que sobra com if block ends with a return statement, so drop this else and outdent its block.
  • O Go não tem o operador ?:. Usa if/else, ou cmp.Or quando 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.gogo
package 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.texttext
attempt 3 failed, retrying

Há três regras diferentes de C, Java e JavaScript:

  1. Sem parênteses à volta da condição. if (attempt < maxAttempts) { compila, mas o gofmt remove os parênteses.
  2. As chavetas são obrigatórias, mesmo para um corpo de uma só linha. if code >= 500 fmt.Println("server error") falha com syntax error: unexpected name fmt, expected {.
  3. A condição tem de ser um bool. O Go não tem valores truthy, por isso if len(items) e if user são erros de tipo. Escreve if len(items) > 0 e if 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.gogo
	if 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.gogo
package 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.texttext
fast=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.gogo
	for _, 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.gogo
package 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.texttext
PORT=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.gogo
	var 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.texttext
loaded 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.gogo
func 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.gogo
func 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.gogo
func 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.texttext
lint.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.gogo
	if 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.gogo
	level := 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.gogo
	var 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.gogo
package main

import (
	"cmp"
	"fmt"
	"os"
)

func main() {
	addr := cmp.Or(os.Getenv("LISTEN_ADDR"), ":8080")
	fmt.Println("listening on", addr)
}
example.texttext
listening 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

Escreve Go como um engenheiro sénior

Lições interativas no navegador. As primeiras são grátis.

Experimenta uma lição grátisOu cria uma conta gratuita