🔎 Buscar

🐹 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.

Wiki / Apuntes📖 Contenido

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 usas var.
  • 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, int8int64 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 cero nil.

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 return desnudo 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 make cuando sepas el tamaño aproximado: append que 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 con make o 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 nil provoca panic. Comprueba nil antes 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. any es ú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 con Is) y envuelve con %w para 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, usa utf8.RuneCountInString(s) o len([]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

Estudio · Recursos de todo el mundo (inglés, chino, japonés, español, francés, ruso…) curados y traducidos al español.