La palabra clave if ejecuta un bloque de código cuando una condición booleana es verdadera. La versión de Go tiene tres reglas que sorprenden a quien viene de C, JavaScript o Python. La condición no lleva paréntesis, las llaves son siempre obligatorias y la condición tiene que ser un bool de verdad, porque Go no tiene valores truthy ni falsy. Un if también puede empezar con una sentencia corta, y por eso if err := f(); err != nil aparece en casi todos los archivos Go (especificación de Go).
Resumen rápido
if status >= 500 { ... }no lleva paréntesis alrededor de la condición, y las llaves son obligatorias incluso para una sola línea.- La condición tiene que ser de tipo
bool.if retries { ... }falla connon-boolean condition in if statement. elsetiene que ir en la misma línea que la}de cierre. En una línea aparte es un error de sintaxis.if v, err := parse(s); err != nil { ... }declara variables que existen en elify en todas sus ramaselse, y en ningún sitio más.:=dentro de un bloqueifpuede ocultar (shadowing) una variable exterior. El código compila y usa en silencio el valor antiguo.- El Go idiomático retorna pronto ante los errores y mantiene el camino feliz sin sangría. Un
elsedespués de unreturnnormalmente se elimina. - Go no tiene operador ternario. Usa una asignación más un
if,cmp.Orpara valores por defecto, ominymaxpara acotar valores. &&y||evalúan en cortocircuito, así quereq.User != nil && req.User.IsAdminnunca desreferencia un puntero nil.- Una cadena larga de
if/else ifsuele leerse mejor como unswitchsin expresión.
¿Cómo se escribe una sentencia if en Go?
Escribe if, una expresión booleana y un bloque entre llaves. Aquí tienes una comprobación de reintentos para un cliente HTTP:
example.gogopackage 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.texttexttrue false false
No hay paréntesis alrededor de attempt >= maxAttempts. Puedes escribirlos y el código sigue compilando, pero gofmt los quita la próxima vez que guardas el archivo. Lo que Go exige son las llaves. if attempt >= maxAttempts return false en una sola línea no se puede analizar, y tampoco una condición seguida de una sentencia en la línea siguiente:
example.gogoif status >= 500 fmt.Println("server error")
example.texttext./main.go:8:19: syntax error: unexpected newline, expected { after if clause
Las llaves obligatorias descartan un bug habitual en C. Añades una segunda línea bajo un if sin llaves, parece parte de la condición y se ejecuta siempre. El bug de TLS «goto fail» de Apple en 2014 fue exactamente ese error. En Go, el cuerpo de cada if tiene límites explícitos, y gofmt los sangra igual en todos los proyectos.
¿Tiene Go valores truthy y falsy?
No. La condición tiene que ser una expresión de tipo bool. Un int, un string, un puntero, un slice o un error nunca se convierten solos en true o false:
example.gogoretries := 3 if retries { fmt.Println("retrying") }
example.texttext./main.go:7:5: non-boolean condition in if statement
Escribes la comparación que quieres decir: retries > 0, name != "", user != nil, len(items) > 0 o err != nil. Cuesta unos pocos caracteres y deja la comprobación explícita. En JavaScript, if (count) se salta el bloque cuando count vale 0, lo que a menudo es un bug cuando el cero es un valor válido. En Go, quien lee el código siempre ve qué condición se comprueba.
¿Cómo funcionan else y else if en Go?
else se ejecuta cuando la condición es falsa, y else if comprueba otra condición. Aquí un logger de peticiones elige el nivel de log según el código de estado:
example.gogofunc 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.texttextlevel=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
Las ramas se comprueban de arriba abajo y se ejecuta la primera verdadera. Un 502 cumple primero status >= 500, así que nunca llega a la comprobación status >= 400. La palabra clave else en Go explica else y las cadenas de else if con más detalle.
¿Por qué else tiene que ir en la misma línea que la llave de cierre?
Por la inserción automática de punto y coma. La gramática de Go usa punto y coma para terminar las sentencias, y el analizador léxico añade uno al final de una línea que termina en }, un identificador, un literal o algunos otros tokens (especificación de Go). Por tanto, una } sola en una línea termina la sentencia if. El else de la línea siguiente empieza entonces una sentencia nueva, y ninguna sentencia puede empezar por else:
example.gogoif status >= 500 { fmt.Println("server error") } else { fmt.Println("ok") }
example.texttext./main.go:10:2: syntax error: unexpected keyword else, expected }
La solución es escribir } else { en una sola línea. La misma regla explica por qué la llave de apertura de un if, un for o una func no puede ir en su propia línea. Además, en los proyectos Go nadie discute sobre el estilo de las llaves. El compilador acepta una sola posición, y gofmt se encarga del resto del formato.
¿Qué es la sentencia if con una sentencia corta?
Un if puede empezar con una sentencia corta, separada de la condición por un punto y coma. La sentencia se ejecuta primero, y las variables que declara tienen como ámbito el if. El uso más habitual es el manejo de errores:
example.gogofunc 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 }
json.Unmarshal solo devuelve un error, así que la llamada y la comprobación caben en una línea, y ese err deja de existir después de la llave de cierre. os.ReadFile devuelve datos que necesitas después, así que va en su propia línea. Esa es la división habitual en el código Go. Pon la sentencia dentro del if cuando sus resultados solo se necesitan para la comprobación, y fuera cuando el resto de la función los usa.
La misma forma funciona con cualquier expresión que devuelva un valor y un booleano, lo que Go llama el modismo comma-ok. Una búsqueda en un map te dice si la clave estaba presente:
example.gogoroles := 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.texttextu_42 is admin u_99 has no role
Una aserción de tipo comprueba si hay un comportamiento opcional sin arriesgarse a un panic. Un handler de streaming solo vacía el buffer cuando el http.ResponseWriter lo admite:
example.gogoif f, ok := w.(http.Flusher); ok { f.Flush() }
Go 1.26 añadió errors.AsType, una versión genérica de errors.As que devuelve el error encontrado y un booleano. Eso hace que encaje en el mismo patrón. Un servicio puede usar valores por defecto cuando falta el archivo de configuración, pero seguir fallando cuando el archivo está roto:
example.gogofor _, 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.texttextno config file at missing.json so using defaults fatal: parse bad.json: invalid character '}' looking for beginning of object key string
Con el antiguo errors.As, declaras var pathErr *fs.PathError antes del if y pasas &pathErr, así que la variable sobrevive a la comprobación.
¿Cuál es el ámbito de una variable declarada en una sentencia if?
La variable existe en la condición, en el bloque if y en cada bloque else if y else asociado. No existe después de que termine la sentencia (especificación de Go):
example.gogoraw := "abc" if n, err := strconv.Atoi(raw); err != nil { fmt.Println("bad MAX_CONNS:", err) } else { fmt.Println("max conns", n) } fmt.Println(n)
La rama else puede usar n, pero la última línea falla con undefined: n. Es deliberado. La variable no puede filtrarse a código que no debería depender de ella, y el nombre vuelve a quedar libre para el siguiente if. Así una función Go puede tener diez comprobaciones if err := ...; err != nil sin diez nombres de error distintos.
¿Cómo causa := dentro de un if bugs de shadowing?
Cada bloque abre un ámbito nuevo, y := siempre declara en el ámbito actual. Cuando asignas a una variable exterior desde dentro de un bloque if con :=, lo que obtienes es una variable nueva con el mismo nombre:
example.gogofunc 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.texttextHTTP_TIMEOUT set to 30s using 5s
err es nueva dentro del bloque, así que := está permitido, y además declara en silencio un segundo timeout. El valor convertido va a la variable interior y desaparece en la llave de cierre. El compilador lo acepta y las comprobaciones por defecto de go vet no lo señalan. La solución es declarar err con var err error y usar =, para que se asigne el timeout exterior. La palabra clave var en Go explica el shadowing con más detalle, incluido el analizador shadow que detecta este caso.
¿Por qué el Go idiomático evita else?
Porque la mayoría de las ramas else en Go siguen a un if que ya ha retornado. Effective Go lo dice así: cuando una sentencia if no continúa en la siguiente sentencia, porque el cuerpo termina en break, continue, goto o return, se omite el else innecesario (Effective Go). El resultado es código donde los errores se manejan y se devuelven en cuanto aparecen, y el camino de éxito baja recto por el borde izquierdo de la función.
Aquí tienes un handler de reembolsos escrito con un if anidado para cada comprobación:
example.gogofunc 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, pero el reembolso en sí está cinco niveles más adentro, y cada mensaje de error queda lejos de la condición que lo provoca. Para ver por qué una petición recibe un 401, tienes que emparejar el último else con el primer if. Invertir cada condición y retornar pronto da el mismo comportamiento:
example.gogofunc 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) }
Si ejecutas las dos versiones con httptest y las mismas cinco peticiones, las respuestas son idénticas:
example.texttext401 unauthorized 403 forbidden 400 invalid JSON body 400 amount_cents must be positive 200 refund of 4999 cents queued for ord_1001
Cada comprobación de la versión plana es una cláusula de guarda, una condición emparejada directamente con su respuesta. Añadir una regla nueva, como un límite de reembolso, significa añadir un bloque if más encima de la última línea en lugar de envolverlo todo en otro nivel.
¿Qué significa "drop this else and outdent its block"?
Es el mensaje de la regla indent-error-flow de revive, el linter que sustituyó a golint y que se ejecuta dentro de golangci-lint. Salta con un bloque if que termina en return seguido de un else:
example.gogofunc 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.texttextmain.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)
La pista entre paréntesis tiene que ver con el ámbito del if. port se declara en la sentencia if, así que quitar el else dejaría return port, nil fuera de su ámbito. La solución es mover port, err := strconv.Atoi(raw) a su propia línea encima del if, y entonces cada comprobación se convierte en una cláusula de guarda normal que retorna.
La regla superfluous-else de revive cubre el mismo patrón después de break, continue, goto, panic y os.Exit, y la regla opcional early-return sugiere invertir un if/else cuya rama else termina en return. Nada de esto significa que else esté mal. El ejemplo de logRequest de más arriba lo usa correctamente, porque cada rama asigna un valor y la ejecución continúa después de la sentencia. La palabra clave else en Go explica cuándo else es la opción correcta y cómo refactorizar los casos en que no lo es.
¿Tiene Go operador ternario?
No. El FAQ de Go responde a esto directamente. Los diseñadores habían visto ?: "used too often to create impenetrably complex expressions", y decidieron que "a language needs only one conditional control flow construct" (FAQ de Go). El sustituto que propone el FAQ es un if que asigna una variable. En la práctica, la forma más corta asigna primero el valor por defecto y luego lo sobrescribe:
example.gogotimeout := 5 * time.Second if cfg.Debug { timeout = 5 * time.Minute }
Dos usos habituales del ternario tienen ahora funciones propias. cmp.Or, añadida en Go 1.22, devuelve el primer argumento que no es el valor cero, lo que cubre la mayoría de los casos de «usa esto o, si no, aquello». Las funciones integradas min y max de Go 1.21 cubren el acotado de valores:
example.gogoport := 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.texttextlistening on :8080 page size 100
cmp.Or acepta cualquier número de argumentos, así que cmp.Or(flagAddr, os.Getenv("ADDR"), ":8080") comprueba un flag, luego el entorno y luego un valor por defecto.
¿Por qué no escribir una función ternaria genérica?
Puedes escribir func ternary[T any](cond bool, a, b T) T, y algunos proyectos lo hacen. Tiene una trampa que la forma con if no tiene. Go evalúa todos los argumentos antes de llamar a una función, así que las dos ramas se ejecutan siempre:
example.gogovar u *User name := ternary(u != nil, u.Name, "anonymous")
example.texttextpanic: runtime error: invalid memory address or nil pointer dereference
Un operador ?: de verdad se saltaría u.Name cuando u es nil. La función lee u.Name primero y falla. Lo mismo pasa con las llamadas costosas, que se ejecutan tanto si su resultado se usa como si no. Un if solo evalúa la rama que toma.
¿Cómo evalúan && y || en cortocircuito dentro de un if?
&& solo evalúa su lado derecho cuando el izquierdo es true, y || solo evalúa su lado derecho cuando el izquierdo es false (especificación de Go). Por eso el orden de las condiciones puede decidir si el código provoca un panic. Una comprobación de nil tiene que ir antes del acceso al campo:
example.gogoif req.User != nil && req.User.IsAdmin { fmt.Println("admin panel") } else { fmt.Println("access denied") }
Con un req.User nil, esto imprime access denied. Si intercambias las dos condiciones, como en req.User.IsAdmin && req.User != nil, la misma petición provoca un panic con invalid memory address or nil pointer dereference, porque el campo se lee antes de que se ejecute la comprobación.
|| funciona igual para rechazar pronto. Esta función auxiliar analiza una cabecera Authorization y se detiene en la primera condición que falla:
example.gogofunc 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" devuelve el token y true. Para "Basic dXNlcjpwYXNz" y para un "Bearer" solo devuelve false. Pon primero las comprobaciones baratas y al final las lentas, como una consulta a la base de datos, para que la llamada lenta solo se ejecute cuando pueda cambiar el resultado. && tiene más precedencia que ||, así que a || b && c significa a || (b && c). Añade paréntesis cuando los mezcles, porque gofmt los conserva.
¿Cuándo usar switch en lugar de if/else if?
Cuando comparas un valor con varios casos, o cuando una cadena tiene más de dos o tres ramas else if. Un switch sin expresión comprueba cada case como un booleano de arriba abajo, exactamente igual que una cadena de if/else if:
example.gogoswitch { case status >= 500: level = slog.LevelError case status >= 400: level = slog.LevelWarn default: level = slog.LevelInfo }
Las condiciones quedan alineadas en una columna, y no hay break, porque un switch de Go se detiene después del primer caso que coincide. Un switch también acepta una sentencia corta, como en switch ext := filepath.Ext(name); ext { ... }, con las mismas reglas de ámbito que if. Reserva if para una o dos condiciones, y sobre todo para las comprobaciones de errores, donde if err != nil es lo que cualquiera que lea Go espera ver. La sección sobre switch de la guía de palabras clave de Go explica los switches de expresión, los switches de tipos y fallthrough.
Dónde encaja LevelUpGo
LevelUpGo enseña Go con ejercicios que ejecutan código Go real en el navegador. Go Basics trata if, else y los operadores lógicos desde el principio, con ejercicios en los que escribes tú mismo las condiciones. Simplification tiene lecciones sobre cómo aplanar código anidado con retornos tempranos, simplificar la lógica booleana y sustituir cadenas largas de if por un switch. El Training Ground tiene ejercicios cortos e independientes para practicar fuera de un curso. Para las otras 24 palabras reservadas, consulta Palabras clave de Go: las 25 explicadas.
Preguntas frecuentes
¿Tiene Go operador ternario?
No. Go no tiene el operador ?:, y el FAQ de Go dice que los diseñadores lo dejaron fuera porque se usaba a menudo para construir expresiones difíciles de leer. Usa una variable con un valor por defecto y un if que lo sobrescriba. Para valores de respaldo, cmp.Or(a, b) devuelve el primer argumento distinto de cero, y min y max se encargan de acotar valores.
¿Por qué Go exige llaves en if?
Las llaves hacen explícito el alcance del cuerpo de cada if, así que añadir una línea no puede cambiar por accidente qué sentencias controla la condición. También permiten que Go prescinda de los paréntesis alrededor de la condición, porque la { marca dónde termina. Después, gofmt formatea todos los if de la misma manera.
¿Tiene Go valores truthy y falsy?
No. La condición de un if tiene que ser de tipo bool. Los números, strings, punteros, slices y errores nunca se convierten automáticamente, y if count { ... } falla con non-boolean condition in if statement. Escribe la comparación de forma explícita, como count > 0, s != "" o err != nil.
¿Se puede declarar una variable en una sentencia if en Go?
Sí. Un if puede empezar con una sentencia corta, como en if err := save(order); err != nil. Las variables declaradas ahí son visibles en la condición, en el bloque if y en cualquier bloque else if o else, y desaparecen cuando termina la sentencia.
¿Por qué else tiene que ir en la misma línea que la llave de cierre en Go?
El analizador léxico de Go inserta un punto y coma al final de una línea que termina en }. Si else empieza la línea siguiente, la sentencia if ya ha terminado, y el compilador informa de syntax error: unexpected keyword else, expected }. Escribir } else { en una sola línea evita el punto y coma insertado.
Fuentes
- 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
- The Go Programming Language Specification, Declarations and scope: https://go.dev/ref/spec#Declarations_and_scope
- The Go Programming Language Specification, Logical operators: https://go.dev/ref/spec#Logical_operators
- Go FAQ, Why does Go not have the ?: operator?: https://go.dev/doc/faq#Does_Go_have_a_ternary_form
- Effective Go, If: https://go.dev/doc/effective_go#if
- Effective Go, Redeclaration and reassignment: https://go.dev/doc/effective_go#redeclaration
- Go 1.22 Release Notes, cmp: https://go.dev/doc/go1.22#minor_library_changes
- revive rule descriptions (indent-error-flow, superfluous-else, early-return): https://github.com/revive-lint/revive/blob/master/RULES_DESCRIPTIONS.md
