🐹 Fundamentos del lenguaje Go
Variables, control de flujo, funciones, slices, maps, structs, punteros, interfaces, errores y strings: el núcleo del lenguaje Go con código real.
Fundamentos del lenguaje Go
Go es un lenguaje compilado, de tipado estático, con recolector de basura y sintaxis minimalista. Nació en Google (2009) para escribir servidores y herramientas de forma simple y eficiente. Este artículo recorre el núcleo del lenguaje: todo lo que necesitas antes de tocar concurrencia o la web.
Variables y constantes
Go declara variables con var o con la forma corta := (inferencia de tipo). Una variable declarada y no usada no compila.
package main
import "fmt"
const Pi = 3.14159 // constante, tipo inferido
const (
StatusOK = 200
StatusError = 500
)
func main() {
var nombre string = "ana" // declaración explícita
var edad int // valor cero: 0
altura := 1.72 // inferencia, tipo float64
nombre, edad = "luis", 30 // asignación múltiple
fmt.Println(nombre, edad, altura)
}
Reglas rápidas:
:=solo dentro de funciones; fuera usasvar.- Cada variable no usada es un error de compilación (el compilador es estricto a propósito).
- Se puede declarar varias a la vez:
a, b := 1, 2.
Tipos básicos
| Tipo | Descripción | Valor cero |
|---|---|---|
bool |
verdadero/falso | false |
string |
cadena inmutable | "" |
int, int8…int64 |
enteros con signo | 0 |
uint, uint8… |
enteros sin signo | 0 |
float32, float64 |
coma flotante | 0 |
byte |
alias de uint8 |
0 |
rune |
alias de int32, un punto Unicode |
0 |
complex64, complex128 |
números complejos | 0+0i |
💡 Los tipos compuestos (
slice,map,chan,func,interface, punteros) tienen como valor ceronil.
Control de flujo
Go solo tiene for (sin while ni do). El if admite una inicialización antes de la condición:
func ejemplo(x int) string {
if x > 10 {
return "grande"
} else if x > 5 {
return "medio"
}
return "pequeño"
}
func loop(n int) int {
suma := 0
for i := 0; i < n; i++ { // clásico
suma += i
}
for suma < 100 { // funciona como while
suma *= 2
}
for { // infinito: sal con break
suma--
if suma <= 0 {
break
}
}
return suma
}
switch
El switch de Go no cae (no necesita break) y evalúa expresiones, no solo constantes:
func dia(d int) string {
switch d {
case 1, 7:
return "fin de semana"
case 2, 3, 4, 5, 6:
return "laborable"
default:
return "inválido"
}
}
func clasifica(x any) string { // switch sin expresión = if encadenado
switch {
case x == nil:
return "nada"
case x.(int) < 0:
return "negativo"
default:
return "otro"
}
}
Funciones
Las funciones pueden devolver varios valores, ser variádicas y usar named returns. El defer garantiza ejecución al salir.
package main
import (
"errors"
"fmt"
"strings"
)
// Múltiples valores de retorno (idiomático: valor + error)
func dividir(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("división por cero")
}
return a / b, nil
}
// Variádica: recibe 0 o más args como slice
func unir(sep string, partes ...string) string {
return strings.Join(partes, sep)
}
// Named returns + defer: el named queda registrado al final
func contar(s string) (n int, err error) {
defer func() {
if n == 0 {
err = errors.New("no hay caracteres")
}
}()
n = len(s)
return // return "desnudo": usa los named
}
func main() {
res, err := dividir(10, 2)
if err != nil {
fmt.Println("error:", err)
}
fmt.Println(res) // 5
fmt.Println(unir("-", "a", "b", "c")) // a-b-c
n, _ := contar("hola")
fmt.Println(n) // 4
}
⚠️ El
returndesnudo con named returns es legible en funciones cortas, pero evítalo en funciones largas: oscurece qué devuelve cada rama.
defer
defer se ejecuta al final de la función, en orden LIFO (último en, primero en salir). Ideal para liberar recursos:
func procesar() {
f := abrirArchivo("datos.txt")
defer f.Close() // se cierra sí o sí al salir
defer fmt.Println("limpieza final")
// ... trabajo ...
}
Arrays vs slices
Un array tiene tamaño fijo; una slice es una vista dinámica sobre un array. En la práctica casi siempre usas slices.
func main() {
// Array: tamaño fijo en el tipo
var a [3]int = [3]int{1, 2, 3}
// a = [4]int{1,2,3,4} // ERROR: no compila
// Slice: se crea con make o con un literal
s1 := []int{1, 2, 3}
s2 := make([]int, 3) // len=3, cap=3
s3 := make([]int, 2, 5) // len=2, cap=5
// append: crece la slice (re-asigna el array si hace falta)
s2 = append(s2, 4, 5)
fmt.Println(s2) // [0 0 0 4 5]
// sub-slice (comparte el array subyacente)
sub := s1[1:3]
fmt.Println(sub) // [2 3]
}
make, len y cap
len(s)— número de elementos actuales.cap(s)— capacidad del array subyacente.make([]T, len, cap)— crea la slice con capacidad para evitar re-alocaciones.
s := make([]int, 0, 100) // crece sin re-alocar hasta 100
fmt.Println(len(s), cap(s)) // 0 100
💡 Preasigna capacidad con
makecuando sepas el tamaño aproximado:appendque fuerza re-alocación cuesta tiempo y memoria extra.
Maps
Los maps son tablas hash. make los crea; leer una clave ausente devuelve el valor cero:
func main() {
edades := make(map[string]int)
edades["ana"] = 30
edades["luis"] = 28
// leer con ok: distingue clave ausente de valor cero
edad, ok := edades["pedro"]
fmt.Println(edad, ok) // 0 false
// literal
precios := map[string]float64{"cafe": 1.5, "te": 1.0}
// borrar
delete(precios, "te")
// iterar (orden aleatorio)
for nombre, p := range precios {
fmt.Println(nombre, p)
}
}
⚠️ Leer de un map no inicializado (
var m map[string]int) devuelve el valor cero, pero escribir en él provoca panic. Siempre crea conmakeo literal.
Structs
Un struct agrupa campos. Se instancia con {} y se accede con .:
type Persona struct {
Nombre string
Edad int
Email string
}
func main() {
p := Persona{Nombre: "ana", Edad: 30}
p.Email = "ana@ejemplo.com"
fmt.Printf("%+v\n", p) // {Nombre:ana Edad:30 Email:ana@ejemplo.com}
// puntero a struct: no se usa & explícito en acceso
p2 := &Persona{Nombre: "luis"}
p2.Edad = 40 // Go dereferencia automáticamente
}
Punteros
Un puntero guarda la dirección de memoria de un valor. & obtiene la dirección, * la dereferencia. Se usan para compartir datos sin copiarlos:
func incrementar(n *int) { // recibe un puntero
*n = *n + 1 // modifica el valor original
}
func main() {
x := 10
incrementar(&x) // pasa la dirección
fmt.Println(x) // 11
// el valor cero de un puntero es nil
var p *int
if p == nil {
fmt.Println("p no apunta a nada")
}
}
⚠️ Dereferenciar un puntero
nilprovoca panic. Compruebanilantes de usar*p.
Métodos: value vs pointer receivers
Un método es una función con un receiver. Puede ser por valor (copia) o por puntero (modifica el original):
type Rect struct{ W, H float64 }
// value receiver: no modifica; llama a una copia
func (r Rect) Area() float64 { return r.W * r.H }
// pointer receiver: puede modificar
func (r *Rect) Escalar(f float64) {
r.W *= f
r.H *= f
}
func main() {
r := Rect{W: 2, H: 3}
fmt.Println(r.Area()) // 6
r.Escalar(2)
fmt.Println(r.Area()) // 24
}
Reglas de oro:
- Usa pointer receiver cuando el método modifica el struct o el struct es grande.
- Una vez que un tipo tiene métodos con pointer receiver, sé consistente: mejor todos con puntero.
Interfaces
Una interfaz es un contrato de métodos. Go lo satisface implícitamente: no hace falta declarar implements. Cualquier tipo con los métodos requeridos la cumple.
type Animal interface {
Sonido() string
}
type Perro struct{}
func (Perro) Sonido() string { return "guau" }
type Gato struct{}
func (Gato) Sonido() string { return "miau" }
func hacerSonido(a Animal) {
fmt.Println(a.Sonido())
}
func main() {
hacerSonido(Perro{}) // guau
hacerSonido(Gato{}) // miau
}
Empty interface, type assertion y type switch
any (alias de interface{}) acepta cualquier valor. Para usarlo con seguridad necesitas type assertion o type switch:
func tipo(v any) {
// type assertion: v.(T) con ok
if s, ok := v.(string); ok {
fmt.Println("es string:", s)
return
}
// type switch: switch sobre el tipo dinámico
switch t := v.(type) {
case int:
fmt.Println("entero:", t)
case string:
fmt.Println("string:", t)
default:
fmt.Println("otro tipo")
}
}
func main() {
tipo("hola")
tipo(42)
tipo(3.14)
}
💡 Prefiere interfaces pequeñas y concretas.
anyes útil para genéricos y casos puntuales, pero abusar de él elimina la seguridad de tipos.
Errores
Go no tiene excepciones: los errores son valores que devuelves y compruebas. La interfaz error tiene un único método: Error() string.
import (
"errors"
"fmt"
)
var ErrNoEncontrado = errors.New("recurso no encontrado")
func buscar(id int) (string, error) {
if id <= 0 {
return "", fmt.Errorf("id inválido: %d", id)
}
if id == 99 {
return "", ErrNoEncontrado
}
return "ok", nil
}
Wrapping con %w, errors.Is y errors.As
%w envuelve el error original para conservar la cadena de causa. errors.Is comprueba igualdad (o cadena), errors.As busca un tipo concreto:
func capaSuperior() error {
_, err := buscar(-1)
if err != nil {
return fmt.Errorf("en capa superior: %w", err) // wrap
}
return nil
}
func main() {
err := capaSuperior()
if errors.Is(err, ErrNoEncontrado) {
fmt.Println("es el error sentinel")
}
// errors.As: captura un tipo específico en la cadena
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("error de ruta:", pathErr.Path)
}
}
💡 El patrón moderno: define errores sentinel con
errors.New(comparables conIs) y envuelve con%wpara no perder contexto.
Strings y runas
Un string es una secuencia de bytes (UTF-8). Una rune es un punto de código Unicode. Recorrer con range itera por runas, no por bytes:
import "fmt"
func main() {
s := "Go 🚀"
fmt.Println(len(s)) // 6: bytes (no caracteres)
for i, r := range s { // range itera por runas
fmt.Printf("%d: %c (%U)\n", i, r, r)
}
// convertir
runas := []rune(s)
fmt.Println(len(runas)) // 4: runas
fmt.Println(string(runas[3])) // 🚀
// bytes individuales
bytes := []byte(s)
fmt.Println(len(bytes)) // 6
}
⚠️
len(string)devuelve bytes, no caracteres. Para contar caracteres, usautf8.RuneCountInString(s)olen([]rune(s)).
fmt: formateo y salida
fmt es el paquete de formato estándar. Verbos clave:
| Verbo | Formatea | Ejemplo |
|---|---|---|
%v |
valor por defecto | 42 |
%+v |
struct con nombres | {Nombre:ana} |
%T |
tipo | int |
%d |
entero decimal | 42 |
%s |
string | hola |
%q |
string con comillas | "hola" |
%f, %.2f |
float | 3.14 |
%x |
hexadecimal | 2a |
func main() {
nombre := "ana"
edad := 30
// Fprintf a un writer, Sprintf a string, Printf a stdout
s := fmt.Sprintf("%s tiene %d años", nombre, edad)
fmt.Println(s)
fmt.Printf("Tipo: %T, valor: %v\n", edad, edad)
fmt.Printf("Struct: %+v\n", Persona{Nombre: nombre, Edad: edad})
}
Cheatsheet
| Quieres… | Usa… |
|---|---|
| Declarar sin tipo | v := 42 |
| Constante | const X = 1 |
| Loop / while | for |
| Múltiples retornos | func() (T, error) |
| Crecimiento dinámico | append(s, v) |
| Capacidad preasignada | make([]T, len, cap) |
| Modificar en método | pointer receiver (r *T) |
| Contrato de métodos | interface |
| Comprobar tipo dinámico | type switch |
| Envolver error | fmt.Errorf("...: %w", err) |
| Comparar errores | errors.Is |
Para profundizar
- A Tour of Go: tutorial oficial interactivo.
- Effective Go: guía de estilo y buenas prácticas oficial.
- Documentación de paquetes: referencia de la librería estándar.
- 100 Go Mistakes: errores comunes y cómo evitarlos.
- The Go Programming Language: el libro de referencia canónico.