Toda variable de Go empieza con un valor cero, los números nunca se convierten solos y las constantes son más flexibles que las variables. Aprende var, :=, enteros con tamaño, overflow, iota y los verbos de Printf para ver valores.
Go es estricto con los tipos en algunos lugares donde otros lenguajes son relajados. Un int nunca se convierte en silencio en un float64. Una variable nunca queda con basura adentro. Una constante que no cabe se detecta antes de que el programa se ejecute.
Este post cubre cómo declarar variables, los tipos básicos, los valores cero, las conversiones, las constantes e iota, más los verbos de fmt.Printf que vas a usar para mirar todo eso. Cada programa de abajo se ejecutó con Go 1.26, y su salida está copiada de esa ejecución.
Declarar variables: var y :=
Go tiene dos formas de declarar una variable. var nombra la variable y, si quieres, su tipo y su valor. := declara y asigna en un paso, y toma el tipo del valor.
package main
import "fmt"
var greeting = "hello"
func main() {
var count int
var name string = "Ada"
age := 36
fmt.Println(greeting, count, name, age)
}
Imprime:
hello 0 Ada 36
count se declaró con un tipo y sin valor, así que tiene 0. name tiene tipo y valor. age := 36 toma el tipo int del literal 36.
Las dos formas no se permiten en los mismos lugares:
varfunciona en cualquier lugar, incluso a nivel de paquete, fuera de cualquier función.greetingestá declarada así.:=funciona solo dentro de una función. Escribecount := 3a nivel de paquete y la compilación falla consyntax error: non-declaration statement outside function body.
Dentro de una función, la mayoría del código Go usa :=. Usa var cuando quieres el valor cero a propósito, o cuando quieres un tipo distinto del que daría el literal, como en var ratio float64 = 2.
:= necesita al menos un nombre nuevo
La forma := declara. No sirve para asignar a una variable que ya existe en el mismo ámbito:
package main
import "fmt"
func main() {
x := 1
x := 2
fmt.Println(x)
}
La compilación falla con:
./main.go:7:4: no new variables on left side of :=
Usa x = 2 para cambiar una variable que ya tienes.
Hay una excepción, y la vas a ver todo el tiempo. Cuando := tiene varios nombres a la izquierda, se permite siempre que al menos uno sea nuevo. A los demás solo se les asigna:
package main
import (
"fmt"
"strconv"
)
func main() {
n, err := strconv.Atoi("42")
fmt.Println(n, err)
m, err := strconv.Atoi("x7")
fmt.Println(m, err)
}
Imprime:
42 <nil>
0 strconv.Atoi: parsing "x7": invalid syntax
La segunda línea declara m y reutiliza err. Por eso el código Go puede llamar una función tras otra y seguir revisando la misma variable err. La parte sobre errores cubre qué hacer con ella.
Los tipos básicos
La mayoría del código Go se arregla con cuatro tipos básicos:
| Tipo | Guarda | Ejemplo |
|---|---|---|
int |
números enteros | 42 |
float64 |
números con parte decimal | 2.99 |
string |
texto | "Ada" |
bool |
true o false |
true |
int tiene 64 bits en las máquinas de 64 bits donde casi siempre vas a ejecutar. Cuando necesitas un tamaño exacto, para un formato de archivo, un protocolo de red o para ahorrar memoria en un slice enorme, Go tiene enteros con tamaño:
- con signo:
int8,int16,int32,int64 - sin signo:
uint8,uint16,uint32,uint64, másuint
Un int8 guarda de -128 a 127. Un uint8 guarda de 0 a 255. También existe float32, pero float64 es el tipo por defecto para decimales, y es en lo que se convierte un literal como 2.99.
Overflow: da la vuelta al ejecutar, falla al compilar
Un entero con tamaño tiene lugar para un rango fijo de valores, y lo que pasa cuando te sales del final depende de si Go puede verlo venir. En tiempo de ejecución, el valor da la vuelta:
package main
import "fmt"
func main() {
var small int8 = 127
small++
fmt.Println(small)
var u uint8 = 0
u--
fmt.Println(u)
}
Imprime:
-128
255
Sumar 1 al int8 más grande da el más chico. Restar 1 a un uint8 que vale 0 da 255. No hay panic ni advertencia. Los bits dan la vuelta como el odómetro de un auto.
Cuando el valor que se desborda es una constante escrita en tu código, el compilador puede revisarlo, y lo hace:
package main
import "fmt"
func main() {
var small int8 = 128
fmt.Println(small)
}
La compilación falla con:
./main.go:6:19: cannot use 128 (untyped int constant) as int8 value in variable declaration (overflows)
Entonces la regla es: las constantes se revisan al compilar, y la aritmética sobre variables da la vuelta en silencio al ejecutar. Si un contador de verdad puede crecer tanto, dale un tipo más grande.
Valores cero: nada queda sin inicializar
Todo tipo en Go tiene un valor cero, y una variable declarada sin valor tiene ese valor cero. No existe un estado “sin inicializar” del que te puedas olvidar:
package main
import "fmt"
func main() {
var i int
var f float64
var s string
var b bool
var p *int
var nums []int
var ages map[string]int
var point struct{ X, Y int }
fmt.Println(i, f, b, p, nums, ages, point)
fmt.Printf("%q\n", s)
fmt.Println(nums == nil, ages == nil, p == nil)
}
Imprime:
0 0 false <nil> [] map[] {0 0}
""
true true true
Los números empiezan en 0. Un bool empieza en false. Una cadena empieza como "", la cadena vacía, y por eso aquí se imprime con %q: Println no mostraría nada. Los punteros, los slices y los maps empiezan como nil. Un struct empieza con cada campo en su propio valor cero, así que point es {0 0}.
Explicado como si tuvieras diez años
Imagina que cada caja nueva que te dan ya trae algo adentro. Una caja para números viene con un 0. Una caja para respuestas de sí o no viene con un “no”. Una caja para palabras viene con una tarjeta en blanco.
Nunca abres una caja nueva y encuentras cosas viejas que dejó otra persona. Así que, aunque te olvides de poner algo, sabes exactamente qué hay.
La versión precisa
Cuando Go reserva memoria para una variable, ya sea con var, new, make o un literal compuesto que deja campos afuera, pone esa memoria en bits cero. Para cada tipo, todos los bits en cero significan algo razonable: 0 para números, false para bool, "" para cadenas, y nil para punteros, slices, maps, canales, funciones e interfaces.
Por eso un programa de Go no puede leer los bytes que sobraron de un valor anterior, y por eso go vet no necesita avisarte de que usas una variable antes de darle valor. También influye en cómo se diseñan los tipos de Go. Un buen tipo es útil en su valor cero: un slice nil es un slice vacío que funciona, y var total int ya está listo para sumarle.
Dónde falla la analogía: algunas cajas llegan vacías y selladas. Un map nil se puede leer, y leer cualquier clave da el valor cero, pero escribir en él provoca un panic. La parte sobre maps muestra por qué y qué hacer.
No hay conversiones implícitas
Go nunca convierte entre tipos numéricos por su cuenta, ni siquiera de int a float64, donde no se podría perder nada:
package main
import "fmt"
func main() {
count := 3
price := 2.5
fmt.Println(count * price)
}
La compilación falla con:
./main.go:8:14: invalid operation: count * price (mismatched types int and float64)
count es un int y price es un float64, y un operador necesita que los dos lados tengan el mismo tipo. Dices qué conversión quieres escribiendo el nombre del tipo como si fuera una llamada a función:
package main
import (
"fmt"
"math"
)
func main() {
count := 3
price := 2.99
total := float64(count) * price
fmt.Println(total)
fmt.Println(int(price), int(-price))
fmt.Println(int(math.Round(price)))
big := 300
fmt.Println(uint8(big))
}
Imprime:
8.97
2 -2
3
44
float64(count) convierte 3 en 3.0, así que la multiplicación funciona.
En la otra dirección, int(price) trunca. Quita la parte decimal, así que 2.99 se vuelve 2 y -2.99 se vuelve -2. Redondea hacia cero, no hacia abajo ni al más cercano. Si quieres redondear, llama primero a math.Round.
La última línea convierte un int que vale 300 en un uint8. Solo sobreviven los 8 bits bajos, y 300 menos 256 es 44. Igual que con el overflow, el compilador lo detecta cuando el valor es una constante: uint8(300) falla con constant 300 overflows uint8. Con una variable no puede saberlo, así que el valor da la vuelta.
La rigidez cuesta unas palabras de más. A cambio, cada conversión en un programa de Go está escrita, así que puedes encontrar el lugar donde un valor perdió precisión.
Constantes: con tipo y sin tipo
Una constante es un valor fijo al compilar, declarado con const. Lo sorprendente es que una constante sin tipo se puede usar con varios tipos:
package main
import "fmt"
const ratio = 2
func main() {
var count int = 5
var price float64 = 1.25
var tiny int8 = 3
fmt.Println(count*ratio, price*ratio, tiny*ratio)
fmt.Printf("%T %T %T\n", count*ratio, price*ratio, tiny*ratio)
}
Imprime:
10 2.5 6
int float64 int8
El mismo ratio multiplicó un int, un float64 y un int8, sin conversiones. Compáralo con el error de count * price de arriba. Una variable con un 2 no podría hacer esto.
Dale un tipo a la constante, y la flexibilidad desaparece:
package main
import "fmt"
const ratio int = 2
func main() {
var price float64 = 1.25
fmt.Println(price * ratio)
}
La compilación falla con:
./main.go:9:14: invalid operation: price * ratio (mismatched types float64 and int)
ratio ahora es un int, y sigue las mismas reglas que cualquier variable int.
Explicado como si tuvieras diez años
Una constante sin tipo es como el número 2 escrito en un papelito adhesivo. Lo puedes pegar en un frasco de canicas, una jarra de agua o una bolsa de harina, y significa “dos” de lo que haya ahí.
Una constante con tipo es el número 2 impreso en una canica. Ahora son dos canicas, y no las puedes echar en la jarra.
La versión precisa
Una constante sin tipo tiene una clase, como entero, punto flotante o cadena, pero todavía no tiene un tipo fijo. Cuando la usas, Go le da el tipo que pide el contexto, siempre que el valor quepa en ese tipo. Cuando nada más lo decide, toma un tipo por defecto: int para 2, float64 para 2.5, string para "hi". De ahí sacó int el age := 36.
Las constantes sin tipo también son exactas. El compilador hace la aritmética de constantes con mucha más precisión que cualquier tipo de Go, así que un valor intermedio puede ser enorme siempre que el final quepa:
package main
import "fmt"
const huge = 1 << 100
func main() {
fmt.Println(huge >> 98)
fmt.Println(huge / (1 << 90))
}
Imprime:
4
1024
1 << 100 no cabe en ningún tipo entero de Go. Como constante no hay problema, porque solo los resultados, 4 y 1024, llegan a ser valores int.
Dónde falla la analogía: el papelito igual tiene que caber. La constante sin tipo 128 no entra en un int8, que es el error de overflow de antes.
iota para enumeraciones
Go no tiene palabra clave enum. En su lugar, dentro de un bloque const, el nombre iota cuenta las líneas: 0 en la primera, 1 en la segunda, y así. Junto con un tipo con nombre, eso te da una enumeración:
package main
import "fmt"
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
)
func main() {
fmt.Println(Sunday, Monday, Tuesday, Wednesday)
fmt.Printf("%T %v\n", Tuesday, Tuesday)
}
Imprime:
0 1 2 3
main.Weekday 2
Solo Sunday tiene = iota escrito. Una constante sin nada después de su nombre repite la expresión de arriba, así que Monday también es Weekday = iota, pero en la línea donde iota vale 1. Las cuatro constantes tienen el tipo Weekday, no int a secas.
Saltar valores
El nombre en blanco _ ocupa una línea, y un valor de iota, sin crear una constante:
package main
import "fmt"
type Level int
const (
_ Level = iota
Debug
Info
_
Error
)
func main() {
fmt.Println(Debug, Info, Error)
var unset Level
fmt.Println(unset == Debug)
}
Imprime:
1 2 4
false
El primer _ gasta el 0, y el segundo gasta el 3. Saltar el 0 es una elección común con una razón real. El valor cero de Level es 0, así que un Level que nadie asignó no significa Debug por accidente. unset == Debug es false.
Tamaños y flags con 1 << iota
iota puede aparecer dentro de cualquier expresión constante, y el desplazamiento de bits es el uso clásico. Aquí cada línea desplaza 1 a la izquierda diez bits más, así que cada tamaño es 1024 veces el anterior:
package main
import "fmt"
type ByteSize int64
const (
_ = iota // 0: thrown away
KB ByteSize = 1 << (10 * iota)
MB
GB
TB
)
func main() {
fmt.Println(KB, MB, GB, TB)
file := 1536 * MB
fmt.Printf("%T\n", file)
fmt.Printf("%.2f GB\n", float64(file)/float64(GB))
}
Imprime:
1024 1048576 1073741824 1099511627776
main.ByteSize
1.50 GB
KB está en la línea donde iota vale 1, así que es 1 << 10. MB repite la expresión con iota en 2, que es 1 << 20. 1536 * MB multiplica una constante sin tipo por un ByteSize, así que el resultado también es un ByteSize.
1 << iota a secas da un bit por constante, que es como el código Go escribe flags que se pueden combinar:
package main
import "fmt"
type Permission uint8
const (
Read Permission = 1 << iota
Write
Execute
)
func main() {
fmt.Println(Read, Write, Execute)
perm := Read | Execute
fmt.Printf("%03b %d\n", perm, perm)
fmt.Println(perm&Write != 0, perm&Execute != 0)
}
Imprime:
1 2 4
101 5
false true
Read | Execute activa dos bits, y %03b los imprime en binario. perm&Write != 0 pregunta si un bit está activado.
Mirar valores con fmt.Printf
fmt.Printf recibe una cadena de formato con verbos que empiezan con %, y unos pocos cubren casi todo lo que vas a querer inspeccionar:
package main
import "fmt"
func main() {
name := "Ada"
age := 36
height := 1.6549
admin := true
tags := []string{"go", "math"}
fmt.Printf("%v %v %v %v %v\n", name, age, height, admin, tags)
fmt.Printf("%T %T %T %T %T\n", name, age, height, admin, tags)
fmt.Printf("%q %q\n", name, tags)
fmt.Printf("%d|%5d|%-5d|\n", age, age, age)
fmt.Printf("%.2f %8.2f\n", height, height)
}
Imprime:
Ada 36 1.6549 true [go math]
string int float64 bool []string
"Ada" ["go" "math"]
36| 36|36 |
1.65 1.65
%vimprime cualquier valor en su formato por defecto. Es lo que usaPrintln.%Timprime el tipo. Es la forma más rápida de responder “¿qué me dio:=?”%qimprime cadenas entre comillas, así que las cadenas vacías y los espacios sueltos se ven. También funciona con un slice de cadenas.%dimprime un entero. Un número en el medio fija un ancho mínimo, y-rellena por la derecha en vez de por la izquierda.%.2fimprime un float con dos decimales, redondeado.%8.2fademás rellena hasta 8 caracteres.
Para structs, %v tiene dos hermanos mayores:
package main
import "fmt"
type Point struct {
X, Y int
}
func main() {
p := Point{3, 4}
fmt.Printf("%v\n", p)
fmt.Printf("%+v\n", p)
fmt.Printf("%#v\n", p)
}
Imprime:
{3 4}
{X:3 Y:4}
main.Point{X:3, Y:4}
%+v agrega los nombres de los campos, lo que hace legible la salida de depuración. %#v imprime el valor como sintaxis de Go, con nombre de tipo y todo.
Usar el verbo equivocado no detiene el programa. fmt.Printf("%d\n", "Ada") imprime %!d(string=Ada) y sigue. Pero go vet lo detecta antes de que ejecutes nada: fmt.Printf format %d has arg "Ada" of wrong type string. Vale la pena el hábito de ejecutar go vet en cada cambio.
byte y rune son otros nombres para enteros
Go tiene dos nombres de tipo que vas a encontrar apenas trabajes con texto, y ninguno es un tipo nuevo. byte es otro nombre para uint8, y rune es otro nombre para int32:
package main
import "fmt"
func main() {
var b byte = 'A'
var r rune = 'é'
fmt.Println(b, r)
fmt.Printf("%T %T\n", b, r)
fmt.Printf("%c %c %q\n", b, r, r)
var u uint8 = b
var i int32 = r
fmt.Println(u, i)
fmt.Println(len("café"))
}
Imprime:
65 233
uint8 int32
A é 'é'
65 233
5
Println muestra números, porque eso es todo lo que son. %T ni siquiera dice byte o rune: informa uint8 e int32, porque un alias es el mismo tipo con un segundo nombre. Por eso var u uint8 = b no necesita conversión. Para ver el carácter, usa %c, o %q para verlo entre comillas.
La última línea adelanta lo que viene. "café" tiene cuatro caracteres pero una longitud de 5, porque len cuenta bytes y é ocupa dos. La parte sobre cadenas, bytes y runes explica por qué.
Qué recordar
varfunciona en cualquier lugar y te da el valor cero cuando no pones valor.:=funciona solo dentro de funciones, y necesita al menos un nombre nuevo a la izquierda.- Todo tipo tiene un valor cero: 0,
false,""onil. Una variable de Go nunca queda sin inicializar. - Los enteros con tamaño dan la vuelta en silencio al ejecutar. Una constante que no cabe es un error de compilación.
- Go nunca convierte números por ti. Escribe
float64(n)oint(f), y recuerda queint(f)trunca hacia cero. - Las constantes sin tipo toman el tipo que pida el contexto, siempre que el valor quepa. Las constantes con tipo se comportan como variables de ese tipo.
iotacuenta las líneas de un bloqueconst. Usa_para saltar valores, muchas veces el 0, y1 << iotapara flags.%v,%Ty%qson la forma más rápida de ver qué es realmente un valor.
En Go, cada valor tiene un tipo que puedes imprimir, y un valor inicial que puedes predecir.