Volver al blog

La palabra clave var en Go: valores cero, := y variables a nivel de paquete

Cómo funciona la palabra clave var en Go: formas de declaración, valores cero, var frente a :=, shadowing, variables a nivel de paquete y orden de inicialización, errores centinela y comprobaciones de interfaces en tiempo de compilación.

La palabra clave var en Go: valores cero, := y variables a nivel de paquete

La palabra clave var declara una variable. Le das un nombre y un tipo, un valor inicial o ambos, como en var port int, var host = "localhost" o var ratio float64 = 0.25. Una variable declarada sin valor empieza con el valor cero de su tipo, que es 0 para los números, "" para las cadenas, false para los booleanos y nil para los punteros, slices, maps, channels, funciones e interfaces. Dentro de las funciones, la mayor parte del código Go usa en su lugar la forma corta name := value, pero var es la única forma de declarar una variable a nivel de paquete (especificación de Go).

Resumen rápido

  • var name Type declara una variable con su valor cero. var name = value deduce el tipo a partir del valor. var name Type = value hace las dos cosas. Un bloque var ( ... ) agrupa varias declaraciones.
  • Toda variable de Go está inicializada. No existe la memoria sin inicializar, y tipos como sync.Mutex, sync.WaitGroup y bytes.Buffer se pueden usar tal cual con su valor cero.
  • := declara y asigna en un solo paso, pero solo dentro de funciones. A nivel de paquete necesitas var.
  • := necesita al menos un nombre nuevo en su lado izquierdo. Los nombres que ya existen en el mismo ámbito se reutilizan, y los de un ámbito exterior quedan ocultos (shadowing).
  • Ocultar una variable exterior con := dentro de un bloque if es un bug habitual. El compilador lo acepta, y el analizador shadow lo detecta.
  • Una variable local sin usar es un error de compilación, declared and not used. Las variables sin usar a nivel de paquete compilan.
  • Las variables a nivel de paquete se inicializan en orden de dependencias antes de que se ejecute cualquier función init(). Los errores centinela y las expresiones regulares compiladas son variables de paquete típicas.
  • var _ http.Handler = (*Server)(nil) hace que el compilador compruebe que un tipo implementa una interfaz.

¿Cómo se declara una variable en Go?

Hay tres formas, y un bloque agrupado que admite cualquier combinación de ellas:

example.gogo
package main

import (
	"fmt"
	"time"
)

var (
	serviceName        = "billing-api"
	maxRetries   int64 = 3
	requestCount int
)

func main() {
	var port int             // type only: starts at 0
	var host = "localhost"   // value only: the type is inferred as string
	var ratio float64 = 0.25 // both
	var readTimeout, writeTimeout time.Duration
	fmt.Println(serviceName, maxRetries, requestCount, port, host, ratio, readTimeout, writeTimeout)
}

Esto imprime billing-api 3 0 0 localhost 0.25 0s 0s.

Con un tipo y sin valor, la variable recibe el valor cero de ese tipo. Con un valor y sin tipo, la variable toma el tipo del valor, y para una constante sin tipo como "localhost" o 0.25 ese es el tipo por defecto: string, int, float64, rune, bool o complex128. Con ambos, el valor se convierte al tipo declarado, y así es como obtienes un int64 o un float32 a partir de un literal numérico simple. var maxRetries int64 = 3 es un int64. var maxRetries = 3 es un int.

Una declaración puede incluir varios nombres. var readTimeout, writeTimeout time.Duration declara dos variables del mismo tipo, las dos a 0s. El bloque agrupado var ( ... ) es habitual a nivel de paquete, donde mantiene juntos los ajustes relacionados. A diferencia de un bloque const, no repite la expresión de la línea anterior, así que cada nombre sin valor recibe el valor cero.

Como cualquier otro nombre en Go, una variable declarada a nivel de paquete se exporta cuando su nombre empieza por mayúscula. http.DefaultClient y os.Args son variables de paquete exportadas. La palabra clave package en Go explica cómo funcionan los nombres exportados entre paquetes.

¿Qué es el valor cero en Go?

El valor cero es el valor que tiene una variable cuando la declaras sin inicializarla. Go no tiene memoria sin inicializar, así que cada variable tiene un valor definido desde el momento en que existe:

TipoValor cero
boolfalse
int, float64 y los demás tipos numéricos0
string"" (la cadena vacía)
punteros, slices, maps, channels, funciones, interfacesnil
arrayscada elemento con su valor cero
structscada campo con su valor cero

La misma regla se aplica a la memoria obtenida con new, a los campos de un struct que se omiten en un literal compuesto y a los elementos de un slice creado con make. Un struct User decodificado de un JSON sin campo email acaba con Email == "", no con basura.

¿Por qué son útiles los valores cero en Go?

Los tipos bien diseñados funcionan con su valor cero, sin constructor, y la biblioteca estándar se apoya en esto. Un sync.Mutex con su valor cero es un mutex desbloqueado, un sync.WaitGroup es un grupo con contador a cero y un bytes.Buffer es un buffer vacío listo para escribir. Un struct formado por campos así se puede usar en cuanto se declara:

example.gogo
package main

import (
	"bytes"
	"fmt"
	"sync"
)

type RequestCounter struct {
	mu    sync.Mutex
	total int
}

func (c *RequestCounter) Inc() {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.total++
}

func main() {
	var counter RequestCounter // ready to use, no constructor needed
	var wg sync.WaitGroup
	for range 10 {
		wg.Go(counter.Inc)
	}
	wg.Wait()

	var buf bytes.Buffer
	fmt.Fprintf(&buf, "handled %d requests", counter.total)
	fmt.Println(buf.String())
}

Esto imprime handled 10 requests. Ninguna de las tres variables necesitó una función New. Effective Go recomienda diseñar tus propios tipos de la misma manera, para que var x T baste para empezar a usar un T.

¿Qué valores cero no puedes usar directamente?

Los maps y, en menor medida, los punteros y los channels. Un slice nil funciona con append, len y range, así que var tags []string seguido de tags = append(tags, "go") es correcto. Un map nil se puede leer pero no escribir, y escribir en él provoca un panic con assignment to entry in nil map. Desreferenciar un puntero nil provoca un panic, y enviar por un channel nil se bloquea para siempre. var frente a make en Go repasa estos casos y cuándo conviene usar make.

¿Qué diferencia hay entre var y := en Go?

:= es la declaración corta de variables. retries := 3 significa lo mismo que var retries = 3: declara una variable nueva y deduce su tipo a partir del valor. Ahorra escribir la palabra clave y el tipo, y es la opción habitual dentro de funciones, sobre todo para los valores que devuelve una llamada a función:

example.gogo
resp, err := http.Get(healthURL)

La diferencia está en dónde se permite cada una. := solo funciona dentro del cuerpo de una función. A nivel de paquete, cada declaración tiene que empezar con una palabra clave, así que esto falla:

example.gogo
package main

port := 8080

func main() {}
example.texttext
./main.go:3:1: syntax error: non-declaration statement outside function body

var port = 8080 a nivel de paquete compila.

Dentro de las funciones las dos formas compilan, y la elección se reduce a unas pocas convenciones:

  1. Usa := cuando tengas un valor que asignar, como cfg := loadConfig() o n, err := w.Write(body).
  2. Usa var cuando quieras el valor cero a propósito, como var buf bytes.Buffer, var wg sync.WaitGroup o var users []User para un slice que vas llenando con append. var le indica a quien lee que el valor cero es el punto de partida.
  3. Usa var con un tipo cuando el tipo por defecto de un literal no es el correcto. var limit int64 = 1 << 40 o var threshold float32 = 0.8 se lee mejor que limit := int64(1 << 40).
  4. Usa var cuando la variable se asigna más tarde en distintas ramas, como en var status string seguido de un if/else que le da valor.

¿Se puede redeclarar una variable con := en Go?

En parte. := necesita al menos una variable nueva en su lado izquierdo, y cualquier nombre ya declarado en el mismo ámbito se asigna en lugar de declararse de nuevo. Esto es lo que permite que una función reutilice un mismo err en varias llamadas:

example.gogo
package main

import "os"

func main() {
	f, err := os.Open("config.yaml")
	if err != nil {
		return
	}
	defer f.Close()

	info, err := f.Stat() // ok: info is new, err is reused
	_ = info
	f, err := os.Open("secrets.yaml")
	_ = f
}
example.texttext
./main.go:14:9: no new variables on left side of :=

info, err := f.Stat() compila porque info es nueva. En el último :=, tanto f como err ya existen, así que no hay nada que declarar. Escribe f, err = os.Open("secrets.yaml") con un = normal para asignarles un valor. var no tiene esa excepción. var err error dos veces en el mismo ámbito falla con err redeclared in this block.

¿Qué es el shadowing de variables en Go?

El shadowing (ocultación de variables) ocurre cuando una declaración en un ámbito interior usa el mismo nombre que una variable de un ámbito exterior. La variable interior oculta a la exterior hasta que termina el ámbito interior. Cada bloque if, for, switch y { } abre un ámbito nuevo, y := siempre declara en el ámbito actual. Juntas, esas dos reglas hacen que este bug sea fácil de escribir:

example.gogo
package main

import (
	"fmt"
	"strconv"
)

func listenAddr(rawPort string) (string, error) {
	port := 8080
	if rawPort != "" {
		port, err := strconv.Atoi(rawPort)
		if err != nil {
			return "", fmt.Errorf("invalid PORT %q: %w", rawPort, err)
		}
		fmt.Println("using PORT", port)
	}
	return fmt.Sprintf(":%d", port), nil
}

func main() {
	addr, err := listenAddr("9090")
	fmt.Println(addr, err)
}

Esto imprime using PORT 9090 y después :8080 <nil>. Dentro del bloque if, err es nueva, así que := está permitido, y además declara un port nuevo. El valor convertido va al port interior, que desaparece en la llave de cierre. La función devuelve el port exterior, que sigue valiendo 8080. La palabra clave if en Go explica las reglas de ámbito de las sentencias if que hacen esto posible.

La solución es asignar a la variable exterior en lugar de declarar una nueva. Declara primero err y usa =:

example.gogo
if rawPort != "" {
	var err error
	port, err = strconv.Atoi(rawPort)
	if err != nil {
		return "", fmt.Errorf("invalid PORT %q: %w", rawPort, err)
	}
}

Las comprobaciones por defecto de go vet no informan del shadowing, porque buena parte del shadowing es intencionado, como un err dentro de un if anidado. El analizador shadow del equipo de Go sí lo hace:

example.texttext
go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
go vet -vettool=$(which shadow) ./...
example.texttext
./main.go:11:3: declaration of "port" shadows declaration at line 9

golangci-lint puede ejecutar la misma comprobación a través del linter govet con shadow activado.

¿Por qué Go dice «declared and not used»?

El compilador rechaza cualquier variable local que nunca se lee. En el listado anterior, la línea que imprime es lo único que hace que el port interior compile. Sin ella, el compilador informa de declared and not used: port y señala el bug. Asignar un valor no cuenta como uso:

example.gogo
package main

import "net/http"

func main() {
	resp, err := http.Get("https://example.com")
	if err != nil {
		return
	}
	retries := 0
	retries = 3
}
example.texttext
./main.go:6:2: declared and not used: resp
./main.go:10:2: declared and not used: retries

La regla solo afecta a las variables locales. Una variable de paquete sin usar, un parámetro de función sin usar y un resultado con nombre sin usar compilan sin problema. Cuando necesitas conservar un valor que todavía no lees, asígnalo al identificador en blanco con _ = resp, o descártalo directamente con _, err := http.Get(url).

¿Cómo funcionan las variables a nivel de paquete en Go?

Una variable declarada fuera de cualquier función pertenece al paquete. Es visible en todos los archivos del paquete, existe durante toda la vida del programa y se inicializa antes de que empiece main. Las variables de paquete se inicializan según sus dependencias, no en el orden en que aparecen en el código fuente:

example.gogo
package main

import (
	"errors"
	"fmt"
)

var (
	usersURL = apiBase + "/users"
	apiBase  = apiHost + "/v2"
	apiHost  = "https://api.internal"
)

var ErrNotFound = errors.New("user not found")

func init() {
	fmt.Println("init sees", usersURL)
}

func main() {
	err := fmt.Errorf("load user 42: %w", ErrNotFound)
	fmt.Println(usersURL, errors.Is(err, ErrNotFound))
}

Esto imprime init sees https://api.internal/v2/users y después https://api.internal/v2/users true. usersURL se declara primero, pero depende de apiBase, que a su vez depende de apiHost, así que el compilador las inicializa en orden inverso. Las variables que no dependen entre sí se inicializan en el orden de declaración. Dos variables que dependen la una de la otra son un error, initialization cycle for a.

Cuando todas las variables de paquete tienen su valor, se ejecutan las funciones init(), en el orden en que aparecen en el código fuente, archivo por archivo. Un paquete puede tener varias funciones init(), incluso en un mismo archivo. Los paquetes importados se inicializan por completo antes, así que cuando se ejecuta tu init(), todos los paquetes que importas ya están listos. La palabra clave import en Go explica cómo interactúa init() con los imports en blanco.

¿Por qué los errores centinela se declaran con var?

Porque errors.New es una llamada a función, y su resultado no puede ser una constante. Un error centinela es una variable de paquete con la que quien llama compara usando errors.Is:

example.gogo
var (
	ErrNotFound   = errors.New("user not found")
	ErrEmailTaken = errors.New("email already registered")
)

errors.Is compara valores de error, y cada llamada a errors.New devuelve un valor distinto, aunque el texto sea el mismo. Por eso el error tiene que crearse una sola vez, a nivel de paquete, y compartirse. Algunos ejemplos de la biblioteca estándar son io.EOF, sql.ErrNoRows y http.ErrServerClosed.

Lo mismo vale para otros valores que son costosos de construir y que no cambian después del arranque. var slugPattern = regexp.MustCompile("^[a-z0-9-]+$") compila la expresión regular una vez, al arrancar el programa. Además provoca un panic en el arranque si el patrón no es válido, en lugar de fallar en la primera petición.

¿Deberías usar variables globales en Go?

Para valores que quedan fijos después del arranque, como errores centinela, patrones compilados y tablas de búsqueda, sí. Para estado mutable, normalmente no. Un var db *sql.DB o un var cfg Config a nivel de paquete lo comparten todas las goroutines y todos los tests del paquete. Las escrituras concurrentes necesitan un mutex, los tests no pueden ejecutarse en paralelo con valores distintos y nada en la firma de una función indica que depende de esa variable global.

La alternativa habitual es poner las dependencias en un struct y pasarlo:

example.gogo
type Server struct {
	db     *sql.DB
	logger *slog.Logger
}

func (s *Server) handleGetUser(w http.ResponseWriter, r *http.Request) {
	// uses s.db and s.logger instead of package-level variables
}

Así cada test puede construir su propio Server con su propia base de datos y su propio logger.

¿Qué significa var _ = en Go?

Declara una variable cuyo nombre es el identificador en blanco _. El valor se evalúa y después se descarta. El uso más habitual es comprobar en tiempo de compilación que un tipo implementa una interfaz:

example.gogo
type Server struct{}

func (s *Server) ServeHTTp(w http.ResponseWriter, r *http.Request) {}

var _ http.Handler = (*Server)(nil)

El nombre del método de arriba tiene una errata, y la declaración la convierte en un error de compilación:

example.texttext
./main.go:9:22: cannot use (*Server)(nil) (value of type *Server) as http.Handler value in variable declaration: *Server does not implement http.Handler (missing method ServeHTTP)
		have ServeHTTp(http.ResponseWriter, *http.Request)
		want ServeHTTP(http.ResponseWriter, *http.Request)

Sin la comprobación, la errata solo aparecería donde un *Server se pasa como http.Handler, algo que podría estar en otro paquete o no ocurrir en ninguna parte. (*Server)(nil) es un puntero nil convertido a *Server, así que la comprobación no hace ninguna reserva de memoria. Usa Server{} en su lugar cuando los métodos tengan receptores por valor.

El identificador en blanco también funciona en declaraciones de varios valores. var _, offset = time.Now().Zone() se queda solo con el segundo resultado.

¿Cómo se declaran varias variables a la vez en Go?

Enumera los nombres separados por comas y da una lista de valores equivalente o una llamada a función que devuelva ese mismo número de valores:

example.gogo
package main

import (
	"fmt"
	"strings"
)

var host, port = "localhost", 8080

func main() {
	var user, domain, found = strings.Cut("[email protected]", "@")
	fmt.Println(host, port, user, domain, found)
}

Esto imprime localhost 8080 ops levelupgo.dev true. Sin tipo, cada variable recibe el tipo de su propio valor, así que host es un string y port es un int. Con tipo, como en var minPort, maxPort int = 1024, 65535, todas lo comparten.

La asignación funciona igual. a, b = b, a intercambia dos variables, porque Go evalúa todos los valores de la derecha antes de asignar ninguno.

Desde Go 1.22, las variables de bucle declaradas con := en una sentencia for son variables nuevas en cada iteración. Un closure o una goroutine que captura job en for _, job := range jobs ve el job de esa iteración, no el último. La palabra clave for en Go muestra el bug con goroutines que esto corrigió y cómo comprobar qué comportamiento tiene un módulo.

¿Cuándo usar var y cuándo const en Go?

Usa const para valores que conoces al escribir el código y que nunca cambian, como timeouts, límites y nombres de cabeceras. Usa var para todo lo demás: valores calculados en tiempo de ejecución, valores leídos de la configuración y cualquier slice, map, struct o puntero, ya que esos no pueden ser constantes. La palabra clave const en Go explica las constantes sin tipo, iota y los enums.

Dónde encaja LevelUpGo

LevelUpGo enseña Go con ejercicios que ejecutan código Go real en el navegador. Go Basics presenta var, := y los valores cero en sus primeras lecciones, junto con las constantes y los tipos. Go Language Deep Dives tiene una lección sobre ámbitos y shadowing en la que encuentras y corriges variables ocultas. 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

¿Es var una palabra clave en Go?

Sí. var es una de las 25 palabras clave reservadas de Go, así que no puedes usarla como nombre de variable, de función ni de tipo. Inicia una declaración de variables, ya sea una sola var name Type = value o un bloque agrupado var ( ... ).

¿Qué diferencia hay entre var y := en Go?

:= es una forma corta de var que siempre deduce el tipo y solo funciona dentro de funciones. x := 5 y var x = 5 declaran los dos un int con el valor 5. var es obligatorio a nivel de paquete y es la opción más clara cuando quieres el valor cero o un tipo distinto del tipo por defecto del literal. := también puede mezclar nombres nuevos y existentes, siempre que al menos un nombre de la izquierda sea nuevo.

¿Cuál es el valor cero de una variable en Go?

Es el valor que tiene una variable cuando se declara sin inicializador: 0 para los números, "" para las cadenas, false para los booleanos y nil para los punteros, slices, maps, channels, funciones e interfaces. Los arrays y los structs reciben el valor cero en cada elemento o campo. Go no tiene variables sin inicializar.

¿Se puede usar := fuera de una función en Go?

No. A nivel de paquete, cada declaración tiene que empezar con una palabra clave como var, const, type o func, y port := 8080 falla con syntax error: non-declaration statement outside function body. Escribe var port = 8080 en su lugar.

¿Por qué Go da un error «declared and not used»?

Porque una variable local sin usar suele indicar un bug, como una variable oculta o una comprobación de error olvidada, así que el compilador de Go la rechaza. Asignar un valor no cuenta como usarla. Lee la variable, elimínala o asígnala al identificador en blanco con _ = x. Las variables de paquete y los parámetros de función sin usar están permitidos.

Fuentes

Escribe Go como un ingeniero sénior

Lecciones interactivas en tu navegador. Las primeras son gratis.

Prueba una lección gratisO crea una cuenta gratuita