Voltar ao blog

A palavra-chave if em Go: condições, instruções de inicialização e retornos antecipados

Como funciona o if em Go: chavetas obrigatórias, condições bool, else, instruções de inicialização, shadowing, retornos antecipados e a falta de ternário.

A palavra-chave if em Go: condições, instruções de inicialização e retornos antecipados

A palavra-chave if executa um bloco de código quando uma condição booleana é verdadeira. A versão do Go tem três regras que surpreendem quem vem de C, JavaScript ou Python. A condição não leva parênteses, as chavetas são sempre obrigatórias e a condição tem de ser um bool a sério, porque o Go não tem valores truthy nem falsy. Um if também pode começar com uma instrução curta, e é por isso que if err := f(); err != nil aparece em quase todos os ficheiros Go (especificação do Go).

Resumo rápido

  • if status >= 500 { ... } não tem parênteses à volta da condição, e as chavetas são obrigatórias mesmo para uma só linha.
  • A condição tem de ser do tipo bool. if retries { ... } falha com non-boolean condition in if statement.
  • O else tem de ficar na mesma linha que a } de fecho. Numa linha própria, é um erro de sintaxe.
  • if v, err := parse(s); err != nil { ... } declara variáveis que existem no if e em todos os seus ramos else, e em mais lado nenhum depois disso.
  • Um := dentro de um bloco if pode fazer shadowing de uma variável exterior. O código compila e usa o valor antigo sem avisar.
  • O Go idiomático retorna cedo nos erros e mantém o caminho de sucesso sem indentação. Um else depois de um return costuma ser removido.
  • O Go não tem operador ternário. Usa uma atribuição mais um if, o cmp.Or para valores por omissão, ou min e max para limitar valores.
  • O && e o || fazem avaliação em curto-circuito, por isso req.User != nil && req.User.IsAdmin nunca desreferencia um ponteiro nil.
  • Uma cadeia longa de if/else if costuma ler-se melhor como um switch sem expressão.

Como se escreve uma instrução if em Go?

Escreve if, uma expressão booleana e um bloco entre chavetas. Eis uma verificação de novas tentativas para um cliente HTTP:

example.gogo
package main

import "fmt"

func shouldRetry(status, attempt, maxAttempts int) bool {
	if attempt >= maxAttempts {
		return false
	}
	return status == 429 || status >= 500
}

func main() {
	fmt.Println(shouldRetry(503, 1, 3))
	fmt.Println(shouldRetry(503, 3, 3))
	fmt.Println(shouldRetry(404, 1, 3))
}
example.texttext
true
false
false

Não há parênteses à volta de attempt >= maxAttempts. Podes escrevê-los e o código continua a compilar, mas o gofmt remove-os da próxima vez que o ficheiro for guardado. As chavetas são a parte em que o Go insiste. if attempt >= maxAttempts return false numa só linha não é analisado, e uma condição seguida de uma instrução na linha seguinte também não:

example.gogo
if status >= 500
	fmt.Println("server error")
example.texttext
./main.go:8:19: syntax error: unexpected newline, expected { after if clause

As chavetas obrigatórias impedem um bug comum em C. Acrescentas uma segunda linha debaixo de um if sem chavetas, ela parece fazer parte da condição, e executa sempre. O bug de TLS «goto fail» da Apple, em 2014, foi exatamente esse erro. Em Go, o corpo de cada if tem limites explícitos, e o gofmt indenta-os da mesma forma em qualquer código.

O Go tem valores truthy e falsy?

Não. A condição tem de ser uma expressão do tipo bool. Um int, uma string, um ponteiro, um slice ou um erro nunca são convertidos em true ou false por ti:

example.gogo
retries := 3
if retries {
	fmt.Println("retrying")
}
example.texttext
./main.go:7:5: non-boolean condition in if statement

Escreves a comparação que queres dizer: retries > 0, name != "", user != nil, len(items) > 0 ou err != nil. Custa uns caracteres e torna a verificação explícita. Em JavaScript, if (count) salta o bloco quando count é 0, o que muitas vezes é um bug quando o zero é um valor válido. Em Go, quem lê vê sempre qual é a condição testada.

Como funcionam o else e o else if em Go?

O else executa quando a condição é falsa, e o else if testa outra condição. Aqui, um logger de pedidos escolhe um nível de log a partir do código de estado:

example.gogo
func logRequest(ctx context.Context, logger *slog.Logger, path string, status int) {
	var level slog.Level
	if status >= 500 {
		level = slog.LevelError
	} else if status >= 400 {
		level = slog.LevelWarn
	} else {
		level = slog.LevelInfo
	}
	logger.Log(ctx, level, "request", "path", path, "status", status)
}
example.texttext
level=INFO msg=request path=/api/orders status=200
level=WARN msg=request path=/api/orders/99 status=404
level=ERROR msg=request path=/api/checkout status=502

Os ramos são verificados de cima para baixo e executa o primeiro que for verdadeiro. Um 502 corresponde primeiro a status >= 500, por isso nunca chega ao teste status >= 400. A palavra-chave else em Go aprofunda o else e as cadeias de else if.

Porque é que o else tem de ficar na mesma linha que a chaveta de fecho?

Por causa da inserção automática de ponto e vírgula. A gramática do Go usa pontos e vírgulas para terminar instruções, e o analisador léxico acrescenta um no fim de uma linha que termina com }, um identificador, um literal ou alguns outros tokens (especificação do Go). Uma } sozinha numa linha termina portanto a instrução if. O else na linha seguinte começa então uma nova instrução, e nenhuma instrução pode começar com else:

example.gogo
if status >= 500 {
	fmt.Println("server error")
}
else {
	fmt.Println("ok")
}
example.texttext
./main.go:10:2: syntax error: unexpected keyword else, expected }

A correção é escrever } else { numa só linha. A mesma regra explica porque é que a chaveta de abertura de um if, for ou func não pode ficar numa linha própria. Também faz com que os projetos Go não discutam o estilo das chavetas. O compilador aceita uma só posição, e o gofmt trata do resto da formatação.

O que é a instrução if com uma instrução curta?

Um if pode começar com uma instrução curta, separada da condição por um ponto e vírgula. A instrução executa primeiro, e as variáveis que declara ficam com o âmbito do if. O uso mais comum é o tratamento de erros:

example.gogo
func loadConfig(path string) (Config, error) {
	var cfg Config
	data, err := os.ReadFile(path)
	if err != nil {
		return cfg, fmt.Errorf("read config: %w", err)
	}
	if err := json.Unmarshal(data, &cfg); err != nil {
		return cfg, fmt.Errorf("parse %s: %w", path, err)
	}
	return cfg, nil
}

O json.Unmarshal só devolve um erro, por isso a chamada e a verificação cabem numa linha, e esse err deixa de existir depois da chaveta de fecho. O os.ReadFile devolve dados de que precisas mais à frente, por isso fica numa linha própria. É esta a divisão habitual no código Go. Põe a instrução dentro do if quando os seus resultados só servem para a verificação, e fora quando o resto da função os usa.

A mesma forma funciona com qualquer expressão que devolva um valor e um booleano, a que em Go se chama o padrão comma-ok. Uma consulta a um map diz-te se a chave existia:

example.gogo
roles := map[string]string{"u_42": "admin", "u_7": "billing"}

if role, ok := roles["u_42"]; ok {
	fmt.Println("u_42 is", role)
}
if _, ok := roles["u_99"]; !ok {
	fmt.Println("u_99 has no role")
}
example.texttext
u_42 is admin
u_99 has no role

Uma type assertion verifica comportamento opcional sem arriscar um pânico. Um handler de streaming só faz flush quando o http.ResponseWriter o suporta:

example.gogo
if f, ok := w.(http.Flusher); ok {
	f.Flush()
}

O Go 1.26 acrescentou o errors.AsType, uma versão genérica do errors.As que devolve o erro encontrado e um booleano. Isso faz com que encaixe no mesmo padrão. Um serviço pode recorrer aos valores por omissão quando o ficheiro de configuração não existe, mas continuar a falhar quando ele está estragado:

example.gogo
for _, path := range []string{"missing.json", "bad.json"} {
	_, err := loadConfig(path)
	if pathErr, ok := errors.AsType[*fs.PathError](err); ok {
		fmt.Println("no config file at", pathErr.Path, "so using defaults")
	} else if err != nil {
		fmt.Println("fatal:", err)
	}
}
example.texttext
no config file at missing.json so using defaults
fatal: parse bad.json: invalid character '}' looking for beginning of object key string

Com o errors.As mais antigo, declaras var pathErr *fs.PathError antes do if e passas &pathErr, por isso a variável sobrevive à verificação.

Qual é o âmbito de uma variável declarada numa instrução if?

A variável existe na condição, no bloco if e em todos os blocos else if e else que lhe estão associados. Deixa de existir quando a instrução termina (especificação do Go):

example.gogo
raw := "abc"
if n, err := strconv.Atoi(raw); err != nil {
	fmt.Println("bad MAX_CONNS:", err)
} else {
	fmt.Println("max conns", n)
}
fmt.Println(n)

O ramo else pode usar o n, mas a última linha falha com undefined: n. Isto é propositado. A variável não pode escapar para código que não deve depender dela, e o nome volta a ficar livre para o próximo if. É assim que uma função Go pode ter dez verificações if err := ...; err != nil sem dez nomes diferentes para o erro.

Como é que um := dentro de um if causa bugs de shadowing?

Cada bloco abre um novo âmbito, e o := declara sempre no âmbito atual. Quando atribuis a uma variável exterior a partir de dentro de um bloco if com :=, o que obténs é uma variável nova com o mesmo nome:

example.gogo
func requestTimeout() time.Duration {
	timeout := 5 * time.Second
	if raw := os.Getenv("HTTP_TIMEOUT"); raw != "" {
		timeout, err := time.ParseDuration(raw)
		if err != nil {
			fmt.Println("ignoring HTTP_TIMEOUT:", err)
			return 5 * time.Second
		}
		fmt.Println("HTTP_TIMEOUT set to", timeout)
	}
	return timeout
}
example.texttext
HTTP_TIMEOUT set to 30s
using 5s

O err é novo dentro do bloco, por isso o := é permitido, e declara também, sem dar nas vistas, um segundo timeout. O valor convertido vai para a variável interior e desaparece na chaveta de fecho. O compilador aceita isto e as verificações por omissão do go vet não o assinalam. A correção é declarar o err com var err error e usar =, para que seja o timeout exterior a receber o valor. A palavra-chave var em Go aprofunda o shadowing, incluindo o analisador shadow que apanha este caso.

Porque é que o Go idiomático evita o else?

Porque a maioria dos ramos else em Go vem a seguir a um if que já retornou. O Effective Go explica assim: quando uma instrução if não continua para a instrução seguinte, porque o corpo termina em break, continue, goto ou return, o else desnecessário é omitido (Effective Go). O resultado é código em que os erros são tratados e devolvidos à medida que surgem, e o caminho de sucesso desce a direito pela margem esquerda da função.

Eis um handler de reembolsos escrito com um if aninhado para cada verificação:

example.gogo
func handleRefund(w http.ResponseWriter, r *http.Request) {
	user, ok := userFromContext(r.Context())
	if ok {
		if user.CanRefund {
			var req RefundRequest
			if err := json.NewDecoder(r.Body).Decode(&req); err == nil {
				if req.AmountCents > 0 {
					fmt.Fprintf(w, "refund of %d cents queued for %s", req.AmountCents, req.OrderID)
				} else {
					http.Error(w, "amount_cents must be positive", http.StatusBadRequest)
				}
			} else {
				http.Error(w, "invalid JSON body", http.StatusBadRequest)
			}
		} else {
			http.Error(w, "forbidden", http.StatusForbidden)
		}
	} else {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
	}
}

Funciona, mas o reembolso propriamente dito fica cinco níveis abaixo, e cada mensagem de erro está longe da condição que a provocou. Para perceber porque é que um pedido recebe um 401, tens de fazer corresponder o último else ao primeiro if. Inverter cada condição e retornar cedo dá o mesmo comportamento:

example.gogo
func handleRefund(w http.ResponseWriter, r *http.Request) {
	user, ok := userFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}
	if !user.CanRefund {
		http.Error(w, "forbidden", http.StatusForbidden)
		return
	}

	var req RefundRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid JSON body", http.StatusBadRequest)
		return
	}
	if req.AmountCents <= 0 {
		http.Error(w, "amount_cents must be positive", http.StatusBadRequest)
		return
	}

	fmt.Fprintf(w, "refund of %d cents queued for %s", req.AmountCents, req.OrderID)
}

Passar as duas versões pelo httptest com os mesmos cinco pedidos dá respostas idênticas:

example.texttext
401 unauthorized
403 forbidden
400 invalid JSON body
400 amount_cents must be positive
200 refund of 4999 cents queued for ord_1001

Cada verificação da versão plana é uma cláusula de guarda (guard clause), uma condição emparelhada diretamente com a sua resposta. Acrescentar uma regra nova, como um limite de reembolso, significa acrescentar mais um bloco if antes da última linha em vez de embrulhar tudo em mais um nível.

O que significa "drop this else and outdent its block"?

É a mensagem da regra indent-error-flow do revive, o linter que substituiu o golint e que corre dentro do golangci-lint. Dispara num bloco if que termina em return seguido de um else:

example.gogo
func parsePort(raw string) (int, error) {
	if port, err := strconv.Atoi(raw); err != nil {
		return 0, fmt.Errorf("invalid port %q: %w", raw, err)
	} else if port < 1 || port > 65535 {
		return 0, errors.New("port out of range")
	} else {
		return port, nil
	}
}
example.texttext
main.go:14:9: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)

A dica entre parênteses tem a ver com o âmbito do if. O port é declarado na instrução if, por isso remover o else deixaria return port, nil fora do seu âmbito. A correção é passar port, err := strconv.Atoi(raw) para uma linha própria acima do if, e cada verificação passa então a ser uma cláusula de guarda simples que retorna.

A regra superfluous-else do revive cobre o mesmo padrão depois de break, continue, goto, panic e os.Exit, e a regra opcional early-return sugere inverter um if/else cujo ramo else termina em return. Nada disto quer dizer que o else está errado. O exemplo logRequest acima usa-o corretamente, porque todos os ramos atribuem um valor e a execução continua depois da instrução. A palavra-chave else em Go explica quando o else é a escolha certa e como refatorar os casos em que não é.

O Go tem operador ternário?

Não. A FAQ do Go responde diretamente a isto. Os criadores tinham visto o ?: ser usado com demasiada frequência para criar expressões impenetravelmente complexas, e decidiram que uma linguagem só precisa de uma construção condicional de controlo de fluxo (FAQ do Go). A alternativa que a FAQ propõe é um if que atribui uma variável. Na prática, a forma mais curta define primeiro o valor por omissão e depois substitui-o:

example.gogo
timeout := 5 * time.Second
if cfg.Debug {
	timeout = 5 * time.Minute
}

Dois usos comuns do ternário têm hoje funções próprias. O cmp.Or, acrescentado no Go 1.22, devolve o primeiro argumento que não é o valor zero, o que cobre a maioria dos casos do tipo «usa isto ou, em alternativa, aquilo». As funções built-in min e max do Go 1.21 tratam de limitar valores:

example.gogo
port := cmp.Or(os.Getenv("PORT"), "8080")
fmt.Println("listening on :" + port)

requested := 500
pageSize := max(1, min(requested, 100))
fmt.Println("page size", pageSize)
example.texttext
listening on :8080
page size 100

O cmp.Or aceita qualquer número de argumentos, por isso cmp.Or(flagAddr, os.Getenv("ADDR"), ":8080") verifica uma flag, depois o ambiente e por fim um valor por omissão.

Porque não escrever uma função ternária genérica?

Podes escrever func ternary[T any](cond bool, a, b T) T, e há bases de código que o fazem. Só que tem uma armadilha que a forma com if não tem. O Go avalia todos os argumentos antes de chamar uma função, por isso os dois ramos executam sempre:

example.gogo
var u *User
name := ternary(u != nil, u.Name, "anonymous")
example.texttext
panic: runtime error: invalid memory address or nil pointer dereference

Um operador ?: a sério saltaria o u.Name quando o u é nil. A função lê primeiro o u.Name e rebenta. O mesmo acontece com chamadas caras, que executam quer o resultado seja usado, quer não. Um if só avalia o ramo que segue.

Como fazem o && e o || curto-circuito num if?

O && só avalia o lado direito quando o lado esquerdo é true, e o || só avalia o lado direito quando o lado esquerdo é false (especificação do Go). Por isso, a ordem das condições pode decidir se o código entra em pânico. Uma verificação de nil tem de vir antes do acesso ao campo:

example.gogo
if req.User != nil && req.User.IsAdmin {
	fmt.Println("admin panel")
} else {
	fmt.Println("access denied")
}

Com um req.User nil, isto imprime access denied. Troca as duas condições, como em req.User.IsAdmin && req.User != nil, e o mesmo pedido entra em pânico com invalid memory address or nil pointer dereference, porque o campo é lido antes de a verificação correr.

O || funciona da mesma forma para rejeitar cedo. Esta função auxiliar analisa um cabeçalho Authorization e para na primeira condição que falha:

example.gogo
func bearerToken(header string) (string, bool) {
	scheme, token, found := strings.Cut(header, " ")
	if !found || !strings.EqualFold(scheme, "Bearer") || token == "" {
		return "", false
	}
	return token, true
}

Para "Bearer eyJhbGciOi" devolve o token e true. Para "Basic dXNlcjpwYXNz" e para um "Bearer" sozinho devolve false. Põe as verificações baratas primeiro e as lentas, como uma consulta à base de dados, no fim, para que a chamada lenta só execute quando pode mudar o resultado. O && tem precedência sobre o ||, por isso a || b && c significa a || (b && c). Acrescenta parênteses quando os misturas, porque o gofmt mantém-nos.

Quando deves usar switch em vez de if/else if?

Quando comparas um valor com vários casos, ou quando uma cadeia tem mais de dois ou três ramos else if. Um switch sem expressão testa cada case como um booleano, de cima para baixo, exatamente como uma cadeia if/else if:

example.gogo
switch {
case status >= 500:
	level = slog.LevelError
case status >= 400:
	level = slog.LevelWarn
default:
	level = slog.LevelInfo
}

As condições ficam alinhadas numa coluna, e não há break, porque um switch em Go para depois do primeiro case que corresponde. Um switch também aceita uma instrução curta, como em switch ext := filepath.Ext(name); ext { ... }, com as mesmas regras de âmbito do if. Guarda o if para uma ou duas condições, e sobretudo para verificações de erros, onde if err != nil é o que qualquer pessoa que leia Go espera ver. A secção sobre o switch no guia das palavras-chave do Go aborda os switches de expressão, os type switches e o fallthrough.

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 e os operadores lógicos logo no início, com exercícios em que escreves as condições tu próprio. O Simplification tem lições sobre achatar código aninhado com retornos antecipados, simplificar lógica booleana e substituir cadeias longas de if por um switch. O Training Ground tem exercícios curtos e independentes para praticares fora de um curso. Para as outras 24 palavras reservadas, vê Palavras-chave do Go: as 25 explicadas.

Perguntas frequentes

O Go tem operador ternário?

Não. O Go não tem o operador ?:, e a FAQ do Go diz que os criadores o deixaram de fora porque era muitas vezes usado para construir expressões difíceis de ler. Usa uma variável com um valor por omissão e um if que o substitui. Para valores alternativos, o cmp.Or(a, b) devolve o primeiro argumento que não é zero, e o min e o max tratam de limitar valores.

Porque é que o Go exige chavetas no if?

As chavetas tornam explícita a extensão do corpo de cada if, por isso acrescentar uma linha não pode mudar sem querer as instruções que a condição controla. Também permitem ao Go dispensar os parênteses à volta da condição, já que a { marca onde a condição termina. Depois, o gofmt formata todos os if da mesma forma.

O Go tem valores truthy e falsy?

Não. A condição de um if tem de ser do tipo bool. Números, strings, ponteiros, slices e erros nunca são convertidos automaticamente, e if count { ... } falha com non-boolean condition in if statement. Escreve a comparação de forma explícita, como count > 0, s != "" ou err != nil.

Uma instrução if pode declarar uma variável em Go?

Sim. Um if pode começar com uma instrução curta, como em if err := save(order); err != nil. As variáveis declaradas aí têm âmbito na condição, no bloco if e em quaisquer blocos else if ou else, e desaparecem quando a instrução termina.

Porque é que o else tem de ficar na mesma linha que a chaveta de fecho em Go?

O analisador léxico do Go insere um ponto e vírgula no fim de uma linha que termina com }. Se o else começar a linha seguinte, a instrução if já terminou, e o compilador indica syntax error: unexpected keyword else, expected }. Escrever } else { numa só linha evita o ponto e vírgula inserido.

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