Cheatsheet Go
Linguagem de programação do Google
Go
Sintaxe Básica
Variables
package main
import "fmt"
func main() {
var nombre string = "Ana"
var edad int = 30
var activo bool = true
var x, y int = 1, 2
fmt.Println(nombre, edad, activo, x, y)
}Declara variables con var, indicando el tipo o dejando que Go lo infiera. Puedes declarar varias en la misma línea. El valor inicial, si se omite, es el zero value del tipo.
Operadores
// Aritméticos + - * / % // Comparación == != > < >= <= // Lógicos && || ! // Bitwise & | ^ << >> // Dirección y desreferencia &x *p
Go tiene operadores aritméticos, de comparación, lógicos (&&, ||, !) y bitwise. % da el resto de la división. & obtiene la dirección de una variable y * desreferencia un puntero.
Conversiones de tipo
var f float64 = 3.99 var i int = int(f) // 3 (trunca) var n int = 65 var c string = string(n) // "A" (rune) // No hay conversión implícita var x int = 10 // var y float64 = x // ERROR var y float64 = float64(x) // OK
Go no hace conversiones automáticas entre tipos: hay que convertir explícitamente con int(), float64(), etc. Convertir float64 a int trunca la parte decimal.
Inferencia con :=
func main() {
precio := 9.99 // float64
nombre := "Ana" // string
contador := 0 // int
ok := true // bool
// varias de una vez
a, b := 1, 2
}El operador := declara e infiere el tipo a partir del valor, pero solo funciona dentro de funciones. Es la forma más común de crear variables locales. A nivel de paquete usa var.
Zero values
var i int // 0 var f float64 // 0.0 var s string // "" var b bool // false var p *int // nil var sl []int // nil var m map[string]int // nil
Toda variable en Go tiene un valor inicial automático, el zero value: 0 para numeros, cadena vacía para string, false para bool y nil para punteros, slices y maps. No hay variables sin inicializar.
strconv
import "strconv"
// int -> string
s := strconv.Itoa(42) // "42"
// string -> int
n, err := strconv.Atoi("123") // 123
// otros
strconv.FormatFloat(3.14, 'f', 2, 64) // "3.14"
strconv.ParseFloat("3.14", 64)
strconv.FormatBool(true) // "true"
strconv.ParseBool("true")El paquete strconv convierte entre strings y tipos básicos. Itoa() transforma un int en string y Atoi() hace lo inverso, devolviendo también un error si el texto no es válido.
Tipos de datos
// Enteros int, int8, int16, int32, int64 uint, uint8, uint16, uint32, uint64 // Numeros con coma float32, float64 // Otros string // texto (inmutable) bool // true / false byte // alias de uint8 rune // alias de int32 (Unicode) complex64, complex128
Go es fuertemente tipado. Los enteros tienen tamaños fijos (int8 a int64); int depende de la arquitectura. float64 es el estándar para decimales y rune representa un carácter Unicode.
Strings básicas
s := "Hola Mundo" len(s) // longitud en bytes s[0] // primer byte s[0:3] // substring "Hola" // Concatenar nombre := "Hola, " + "Ana" // String bruta (multilínea) texto := `linea 1 linea 2`
Las strings son secuencias inmutables de bytes. len() devuelve el tamaño en bytes y s[0:3] extrae una parte. Las comillas invertidas crean strings brutas que aceptan varias líneas sin escapes.
Salida con fmt
import "fmt"
fmt.Println("Hola") // con newline
fmt.Print("sem ", "newline")
fmt.Printf("Nombre: %s\n", "Ana")
// Verbos comunes
fmt.Printf("%d\n", 42) // entero
fmt.Printf("%.2f\n", 3.14159) // 3.14
fmt.Printf("%t\n", true) // bool
fmt.Printf("%v\n", objeto) // valor por defecto
fmt.Printf("%+v\n", struct1) // con campos
fmt.Printf("%T\n", 3.14) // tipo
fmt.Sprintf("x=%d", 5) // devuelve un stringEl paquete fmt imprime y formatea. Println() añade salto de línea y Printf() usa verbos: %d entero, %s string, %f float, %v valor por defecto. Sprintf() devuelve la string formateada.
Constantes e iota
const Pi = 3.14159
const (
StatusOK = 200
StatusNotFound = 404
)
// iota: contador automático
const (
Domingo = iota // 0
Segunda // 1
Martes // 2
Quarta // 3
)Las constantes se declaran con const y no pueden cambiar. Dentro de un bloque, iota genera numeros secuenciales automáticos (0, 1, 2...), ideal para enumeraciones.
Paquete strings
import "strings"
s := "Hola Mundo Go"
strings.Contains(s, "Mundo") // true
strings.HasPrefix(s, "Hola") // true
strings.HasSuffix(s, "Go") // true
strings.Split(s, " ") // [Hola Mundo Go]
strings.Join([]string{"a", "b"}, "-") // "a-b"
strings.Replace(s, "o", "0", -1)
strings.ToUpper(s)
strings.ToLower(s)
strings.Trim(" x ", " ") // "x"El paquete strings trae funciones para manipular texto: Contains() búsqueda, Split() divide y Join() une. Replace() con -1 sustituye todas las ocurrencias.
Controlo de Fluxo
if / else
edad := 20
if edad >= 18 {
fmt.Println("adulto")
} else if edad > 12 {
fmt.Println("joven")
} else {
fmt.Println("niño")
}El if en Go no usa paréntesis en la condición, pero las llaves son obligatorias. Puedes encadenar else if y else.
for como while e infinito
// while
x := 10
for x > 0 {
x--
}
// bucle infinito
for {
if condición {
break
}
}Go no tiene while: se usa for solo con la condición. Un for sin nada crea un bucle infinito, que termina con break o return.
defer
f, err := os.Open("fichero.txt")
if err != nil {
return err
}
defer f.Close() // se ejecuta al final
// varios defer: orden LIFO
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
// salida: 3, 2, 1defer aplaza una llamada hasta el final de la función, ideal para limpieza (cerrar ficheros y conexiones). Varios defer se ejecutan en orden inverso (LIFO).
if con inicialización
if err := fazer(); err != nil {
log.Fatal(err)
}
// err no existe aquí
if n, ok := m["clave"]; ok {
fmt.Println(n)
}Puedes declarar una variable antes de la condición, separada por ;. Queda limitada al bloque del if. Es el patrón idiomático para verificar errores y resultados.
break y continue
for i := 0; i < 10; i++ {
if i == 3 {
continue // salta el 3
}
if i == 7 {
break // para en el 7
}
fmt.Println(i)
}
// break con label (bucles anidados)
Loop:
for _, a := range x {
for _, b := range a {
if b == alvo {
break Loop
}
}
}continue salta a la siguiente iteración y break termina el bucle. En bucles anidados, un label (como Loop:) permite a break salir del bucle exterior.
for clásico
for i := 0; i < 10; i++ {
fmt.Println(i)
}
// contador decreciente
for i := 10; i > 0; i-- {
fmt.Println(i)
}El for es el único bucle en Go. En su forma clásica tiene tres partes: inicialización, condición y post-instrucción, separadas por ;. No usa paréntesis.
switch
dia := 2
switch dia {
case 1:
fmt.Println("Segunda")
case 2, 3:
fmt.Println("Martes ou Quarta")
case 4, 5:
fmt.Println("Quinta ou Sexta")
default:
fmt.Println("Fin de semana")
}El switch compara un valor con varios case. En Go, cada caso termina automáticamente (sin break). Puedes agrupar varios valores en el mismo case, separados por comas.
for range
// slice
for i, v := range slice {
fmt.Println(i, v)
}
// map
for k, v := range mapa {
fmt.Println(k, v)
}
// string (runes)
for i, r := range "Hola" {
fmt.Println(i, string(r))
}
// ignorar el índice
for _, v := range slice { }range recorre slices, maps, strings y channels, devolviendo índice y valor. Usa _ para ignorar el índice. En strings, itera por rune (carácter Unicode).
switch sin condición
hora := 14
switch {
case hora < 12:
fmt.Println("Bom dia")
case hora < 20:
fmt.Println("Boa tarde")
default:
fmt.Println("Boa noite")
}
// fallthrough (raro)
switch x {
case 1:
fmt.Println("um")
fallthrough
case 2:
fmt.Println("dois")
}Un switch sin expresión funciona como una cadena de if/else if, evaluando cada condición en orden. fallthrough fuerza la ejecución del caso siguiente (poco usado).
Funções
Función simple
func sumar(a, b int) int {
return a + b
}
// parámetros del mismo tipo
func media(a, b, c float64) float64 {
return (a + b + c) / 3
}
resultado := sumar(3, 4) // 7Una función se declara con func, seguida del nombre, parámetros y tipo de retorno. Los parámetros del mismo tipo pueden compartir el tipo (a, b int). El retorno viene al final.
Funciones como valor
var fn func(int) int
fn = func(x int) int {
return x * 2
}
fmt.Println(fn(5)) // 10
// en maps
operaciones := map[string]func(int, int) int{
"suma": func(a, b int) int { return a + b },
}Las funciones son valores de primera clase en Go: puedes guardarlas en variables, maps o slices. El tipo func(int) int describe una función que recibe y devuelve un int.
Recursión
func factorial(n int) int {
if n <= 1 {
return 1
}
return n * factorial(n-1)
}
func fibonacci(n int) int {
if n < 2 {
return n
}
return fibonacci(n-1) + fibonacci(n-2)
}
fmt.Println(factorial(5)) // 120Una función recursiva se llama a sí misma hasta alcanzar un caso base. factorial() multiplica hasta llegar a 1. La recursión es elegante, pero para casos simples un bucle puede ser más eficiente.
Múltiples retornos
func dividir(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("división por zero")
}
return a / b, nil
}
resultado, err := dividir(10, 2)
if err != nil {
log.Fatal(err)
}Go permite devolver varios valores. El patrón idiomático es retornar (resultado, error), donde error es nil cuando todo va bien. El llamador siempre verifica el error.
Funciones anónimas
// ejecutar inmediatamente
func() {
fmt.Println("executada ya")
}()
// con un argumento
func(nombre string) {
fmt.Println("Hola,", nombre)
}("Ana")
// asignar a una variable
doble := func(x int) int { return x * 2 }
fmt.Println(doble(4)) // 8Una función anónima no tiene nombre y puede definirse y llamarse en el mismo lugar. Se usa mucho en go func() {...}() para goroutines y como argumento de otras funciones.
Retornos nombrados
func calcular(a, b int) (suma, producto int) {
suma = a + b
producto = a * b
return // devuelve suma y producto
}
s, p := calcular(3, 4) // 7, 12Los retornos pueden tener nombre, sirviendo como variables locales. Un return sin valores devuelve los valores nombrados actuales. Útil para documentar el significado de cada retorno.
Closures
func contador() func() int {
n := 0
return func() int {
n++
return n
}
}
inc := contador()
fmt.Println(inc()) // 1
fmt.Println(inc()) // 2
fmt.Println(inc()) // 3Una closure es una función que captura variables del entorno donde fue creada. Aquí, la función anónima mantiene el estado de n entre llamadas sucesivas.
Funciones variádicas
func suma(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
suma(1, 2, 3) // 6
suma(1, 2, 3, 4, 5) // 15
// expandir un slice
vals := []int{1, 2, 3}
suma(vals...)Los parámetros con ... aceptan un numero variable de argumentos, recibidos como un slice. Puedes pasar un slice existente expandiéndolo con vals.... fmt.Println() es variádica.
Higher-order functions
// recibir una función como parámetro
func aplicar(nums []int, fn func(int) int) []int {
res := make([]int, len(nums))
for i, v := range nums {
res[i] = fn(v)
}
return res
}
doble := aplicar([]int{1, 2, 3}, func(x int) int {
return x * 2
}) // [2, 4, 6]Las funciones de orden superior reciben o devuelven otras funciones. Pasar una función como parámetro permite generalizar comportamiento, como transformar cada elemento de un slice.
Structs e Interfaces
Struct
type Persona struct {
Nombre string
Edad int
Activo bool
}
// instanciar
p := Persona{
Nombre: "Ana",
Edad: 30,
Activo: true,
}
fmt.Println(p.Nombre) // AnaUna struct agrupa campos de tipos diferentes en un único tipo. Se crea con type y se instancia indicando los campos por nombre. Es la base para modelar datos en Go.
Constructores
type Usuario struct {
Nombre string
Email string
}
// convención: New + nombre del tipo
func NewUsuario(nombre, email string) *Usuario {
return &Usuario{
Nombre: nombre,
Email: email,
}
}
u := NewUsuario("Ana", "ana@mail.com")Go no tiene constructores nativos; la convención es una función NewX() que devuelve un puntero a la struct ya inicializada. Permite validar datos y definir valores por defecto.
Type assertion
var i any = "hola"
// afirmar el tipo
s := i.(string)
fmt.Println(s)
// forma segura (comma-ok)
s, ok := i.(string)
if ok {
fmt.Println("es string:", s)
}
n, ok := i.(int) // ok == falseUna type assertion (i.(string)) extrae el valor concreto de una interface. La forma segura con ok evita un panic si el tipo no corresponde.
Formas de instanciar
type Ponto struct{ X, Y int }
// con nombres de campo (recomendado)
p1 := Ponto{X: 1, Y: 2}
// por posición (orden de los campos)
p2 := Ponto{1, 2}
// zero value
var p3 Ponto // {0, 0}
// con new (devuelve un puntero)
p4 := new(Ponto)
p4.X = 5Puedes instanciar una struct con los nombres de los campos (más claro) o por posición. var p Ponto crea el zero value y new() devuelve un puntero a una struct puesta a cero.
Interfaces
type Forma interface {
Area() float64
Perímetro() float64
}
type Cuadrado struct{ Lado float64 }
func (q Cuadrado) Area() float64 {
return q.Lado * q.Lado
}
func (q Cuadrado) Perímetro() float64 {
return 4 * q.Lado
}
// Cuadrado implementa Forma implícitamenteUna interface define un conjunto de métodos. En Go, la implementación es implícita: cualquier type con esos métodos satisface la interface, sin declaración explícita. Esto favorece el desacoplamiento.
Type switch
func describir(i any) {
switch v := i.(type) {
case int:
fmt.Println("entero:", v)
case string:
fmt.Println("texto:", v)
case bool:
fmt.Println("bool:", v)
case []int:
fmt.Println("slice de int")
default:
fmt.Printf("outro: %T\n", v)
}
}Un type switch (i.(type)) prueba el tipo concreto de una interface en varios case. La variable v asume el tipo correcto en cada rama. %T muestra el tipo.
Métodos (receiver por valor)
type Círculo struct {
Radio float64
}
func (c Círculo) Area() float64 {
return math.Pi * c.Radio * c.Radio
}
c := Círculo{Radio: 5}
fmt.Println(c.Area())Un método es una función con un receiver, declarado entre func y el nombre. Con receiver por valor (c Círculo), el método trabaja sobre una copia y no altera el original.
Interface vacía (any)
// any es alias de interface{}
func imprimir(v any) {
fmt.Println(v)
}
imprimir(42)
imprimir("texto")
imprimir([]int{1, 2, 3})
var x any = "hola"La interface vacía any (alias de interface{}) acepta cualquier tipo, pues no exige métodos. Es útil para funciones genéricas, pero pierde seguridad de tipos — úsala con moderación.
Métodos (receiver por puntero)
type Contador struct {
Valor int
}
func (c *Contador) Incrementar() {
c.Valor++ // altera el original
}
ct := Contador{}
ct.Incrementar()
ct.Incrementar()
fmt.Println(ct.Valor) // 2Con receiver por puntero (c *Contador), el método puede modificar el original y evita copiar structs grandes. Usa puntero siempre que necesites alterar el estado.
Embedding (composición)
type Animal struct {
Nombre string
}
func (a Animal) Describir() string {
return "Soy " + a.Nombre
}
type Perro struct {
Animal // campo embebido
Raca string
}
c := Perro{Animal: Animal{Nombre: "Rex"}, Raca: "Labrador"}
fmt.Println(c.Nombre) // promovido
fmt.Println(c.Describir()) // método promovidoGo usa composición en vez de herencia. Embeber un tipo (sin nombre de campo) promueve sus campos y métodos al tipo exterior. Perro gana Nombre y Describir() de Animal.
Coleções
Arrays
var arr [5]int // [0,0,0,0,0]
arr[0] = 10
nums := [3]int{1, 2, 3}
// tamaño inferido
vals := [...]int{1, 2, 3, 4}
len(nums) // 3
// el tamaño forma parte del tipo
var a [3]int
var b [4]int
// a = b // ERROR: tipos diferentesUn array tiene un tamaño fijo que forma parte del tipo ([3]int es distinto de [4]int). Se pasa por copia. En la práctica, casi siempre se usa un slice, que es dinámico.
make y copy
// make: crea un slice con len y cap
s := make([]int, 5) // len=5, cap=5
s = make([]int, 0, 10) // len=0, cap=10
// copy: copia elementos
src := []int{1, 2, 3}
dst := make([]int, len(src))
n := copy(dst, src) // n=3
// map predimensionado
m := make(map[string]int, 100)make() crea slices, maps y channels con un tamaño inicial, evitando realocaciones. copy(dst, src) copia elementos entre slices de forma segura, sin compartir el array.
Ordenación (sort)
import "sort"
nums := []int{3, 1, 4, 1, 5}
sort.Ints(nums) // [1,1,3,4,5]
sort.Strings([]string{"b", "a", "c"})
// ordenación personalizada
personas := []Persona{{"Ana", 30}, {"Ray", 25}}
sort.Slice(personas, func(i, j int) bool {
return personas[i].Edad < personas[j].Edad
})
sort.SearchInts(nums, 3) // búsqueda binariaEl paquete sort ordena slices: Ints() y Strings() para tipos básicos, sort.Slice() con una función de comparación para tipos personalizados. SearchInts() hace una búsqueda binaria.
Slices
s := []int{1, 2, 3}
// slice vacío
var vacio []int // nil
outro := []int{} // vacío, no nil
len(s) // 3 (longitud)
cap(s) // capacidad
s[0] // 1
s[2] // 3Un slice es una vista dinámica sobre un array, con longitud y capacidad. Es la colección más usada en Go. Se declara con []tipo sin tamaño fijo.
Maps
m := map[string]int{
"manzana": 3,
"banana": 5,
}
// map vacío
vacio := make(map[string]int)
m["naranja"] = 2 // añadir
m["manzana"] = 10 // actualizar
fmt.Println(m["banana"]) // 5
len(m)Un map guarda pares clave-valor. Se crea con map[clave]valor{} o make(). El acceso y la asignación usan la sintaxis m[clave]. Las claves deben ser comparables.
Paquete slices (Go 1.21+)
import "slices"
s := []int{3, 1, 2}
slices.Sort(s) // ordena
slices.Contains(s, 2) // true
slices.Index(s, 2) // posición
slices.Reverse(s) // invierte
slices.Min(s) // mínimo
slices.Max(s) // máximo
slices.Equal(a, b) // compara
slices.Clone(s) // copia independienteEl paquete slices (Go 1.21+) trae funciones genéricas modernas: Contains(), Sort(), Min(), Max() y Clone(). Clone() crea una copia que no comparte el array.
append
s := []int{1, 2, 3}
s = append(s, 4) // [1,2,3,4]
s = append(s, 5, 6) // varios
s = append(s, []int{7, 8}...) // otro slice
// importante: reasignar!
s = append(s, 9)append() añade elementos al final de un slice y devuelve el nuevo slice — hay que reasignar (s = append(s, ...)). Si la capacidad llega al límite, Go asigna un array mayor automáticamente.
Maps: delete y comma-ok
m := map[string]int{"a": 1, "b": 2}
// comma-ok: ¿la clave existe?
v, ok := m["a"]
if ok {
fmt.Println("existe:", v)
}
// zero value si no existe
v = m["z"] // 0 (sin error!)
delete(m, "a") // quitarAcceder a una clave inexistente devuelve el zero value sin error. Por eso se usa el patrón comma-ok (v, ok := m[clave]) para saber si la clave existe. delete() elimina una entrada.
Fatiar (slicing)
s := []int{0, 1, 2, 3, 4, 5}
s[1:4] // [1,2,3]
s[:3] // [0,1,2]
s[3:] // [3,4,5]
s[:] // vista completa
s[1:4:4] // low:high:max (limita cap)
// comparte el array subyacente!
sub := s[1:3]
sub[0] = 99 // altera s tambiénLa sintaxis s[low:high] crea un sub-slice. Atención: el sub-slice comparte el array original, por lo que los cambios afectan a ambos. Usa copy() o la forma de tres índices para aislar.
Recorrer colecciones
// slice
for i, v := range []int{10, 20, 30} {
fmt.Println(i, v)
}
// map (¡orden aleatorio!)
for k, v := range m {
fmt.Println(k, v)
}
// solo claves
for k := range m {
fmt.Println(k)
}range recorre slices (índice, valor) y maps (clave, valor). El orden de iteración de un map es intencionalmente aleatorio. Usa _ para ignorar el índice o la clave.
Concorrência
Goroutines
func tarea(n int) {
fmt.Println("tarea", n)
}
func main() {
go tarea(1) // goroutine
go func() {
fmt.Println("asíncrono")
}()
time.Sleep(time.Second) // espera simple
}Una goroutine es una función ejecutada concurrentemente, lanzada con go. Son extremadamente ligeras (pocos KB). El programa termina cuando main() acaba, por eso hay que sincronizar.
Cerrar y recorrer channels
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) // cerrar al terminar
}()
// range termina cuando el channel cierra
for v := range ch {
fmt.Println(v)
}
// comma-ok: ¿channel cerrado?
v, ok := <-chclose(ch) indica que no habrá más envíos. Un for range sobre el channel termina automáticamente cuando cierra. El patrón v, ok := <-ch devuelve ok == false si está cerrado y vacío.
atomic y RWMutex
import "sync/atomic" var total int64 atomic.AddInt64(&total, 1) // incremento atómico v := atomic.LoadInt64(&total) // RWMutex: varias lecturas, 1 escritura var rw sync.RWMutex rw.RLock() // lectura // ... leer datos ... rw.RUnlock() rw.Lock() // escritura // ... escribir ... rw.Unlock()
sync/atomic hace operaciones atómicas sin locks, ideal para contadores simples. sync.RWMutex permite varias lecturas simultáneas (RLock()) pero solo una escritura (Lock()).
Channels
ch := make(chan string)
go func() {
ch <- "hola" // envía (bloquea)
}()
msg := <-ch // recibe (bloquea)
fmt.Println(msg)Un channel comunica entre goroutines de forma segura. ch <- valor envía y <-ch recibe. Un channel sin buffer bloquea hasta que haya un receptor y un emisor listos.
select
select {
case msg := <-ch1:
fmt.Println("ch1:", msg)
case msg := <-ch2:
fmt.Println("ch2:", msg)
case <-time.After(time.Second):
fmt.Println("timeout")
default:
fmt.Println("nenhum pronto")
}select espera por varias operaciones de channel, ejecutando el primer caso listo. Si varios están listos, elige uno al azar. default evita el bloqueo y time.After() crea timeouts.
Context
ctx, cancel := context.WithTimeout(
context.Background(),
5*time.Second,
)
defer cancel()
select {
case res := <-fazer(ctx):
fmt.Println(res)
case <-ctx.Done():
fmt.Println("cancelado:", ctx.Err())
}El paquete context controla la cancelación y los timeouts entre goroutines. WithTimeout() crea un contexto que expira; ctx.Done() es un channel que cierra al cancelar. Pasa ctx como primer parámetro.
Channel con buffer
ch := make(chan int, 3) // buffer de 3 ch <- 1 // no bloquea ch <- 2 ch <- 3 // ch <- 4 // BLOQUEA (lleno) fmt.Println(<-ch) // 1 (FIFO) fmt.Println(len(ch), cap(ch)) // 2 3
Un channel con buffer (make(chan int, 3)) acepta envíos sin bloquear mientras haya espacio. Solo bloquea cuando está lleno (envío) o vacío (recepción).
WaitGroup
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
fmt.Println(n)
}(i)
}
wg.Wait() // bloquea hasta que todas terminen
fmt.Println("hecho")sync.WaitGroup espera a que un conjunto de goroutines termine. Add(1) registra cada una, Done() (en defer) marca la finalización y Wait() bloquea hasta que todas acaben.
Channels direccionales
// solo envía
func produtor(ch chan<- int) {
ch <- 42
}
// solo recibe
func consumidor(ch <-chan int) {
fmt.Println(<-ch)
}
ch := make(chan int)
go produtor(ch)
consumidor(ch)Puedes restringir la dirección de un channel en el tipo: chan<- int solo envía, <-chan int solo recibe. Esto hace las APIs más claras y seguras, indicando quién produce y quién consume.
Mutex
var (
mu sync.Mutex
contador int
)
func incrementar() {
mu.Lock()
defer mu.Unlock()
contador++
}
// 1000 goroutines seguras
for i := 0; i < 1000; i++ {
go incrementar()
}Un sync.Mutex protege datos compartidos: Lock() bloquea el acceso y Unlock() lo libera. Usa defer mu.Unlock() para garantizar la liberación incluso en caso de error.
Tratamento de Erros
Patrón error
f, err := os.Open("datos.txt")
if err != nil {
return fmt.Errorf("abrir fichero: %w", err)
}
defer f.Close()
// usar fEn Go, los errores son valores. Las funciones que pueden fallar devuelven un error como último valor de retorno. El patrón idiomático es comprobar if err != nil inmediatamente y propagar el error.
errors.Is
import (
"errors"
"os"
)
_, err := os.Open("inexistente.txt")
if errors.Is(err, os.ErrNotExist) {
fmt.Println("fichero no existe")
}
// compara a lo largo de la cadena de wrappingerrors.Is(err, objetivo) comprueba si un error (o cualquier error envuelto en la cadena) corresponde a un valor específico, como os.ErrNotExist. Sustituye la comparación directa, que no funciona con wrapping.
Crear errores
import (
"errors"
"fmt"
)
// error simple
err := errors.New("algo falhou")
// error formateado
err = fmt.Errorf("valor %d es inválido", n)
return errerrors.New() crea un error con un mensaje fijo. fmt.Errorf() crea errores formateados, como Sprintf. Ambos devuelven un valor que implementa la interface error.
errors.As
var nf *NotFoundError
if errors.As(err, &nf) {
// nf ahora tiene el tipo concreto
fmt.Println("faltou:", nf.Nombre)
}
// extrae el primer error del tipo indicadoerrors.As(err, &objetivo) búsqueda en la cadena el primer error de un tipo concreto y lo copia en objetivo. Permite acceder a campos específicos de errores personalizados incluso después de wrapping.
Wrapping con %w
func lerConfig() error {
_, err := os.Open("config.yaml")
if err != nil {
return fmt.Errorf("leer config: %w", err)
}
return nil
}
// %w envuelve el error original,
// preservando la cadena de causasEl verbo %w en fmt.Errorf() envuelve el error original, creando una cadena. Esto permite inspeccionar la causa más tarde con errors.Is() y errors.As(), sin perder contexto.
errors.Join (Go 1.20+)
import "errors"
err1 := validarNome(nombre)
err2 := validarEmail(email)
// combinar varios errores
err := errors.Join(err1, err2)
if err != nil {
return err // muestra ambos
}
errors.Is(err, err1) // trueerrors.Join() (Go 1.20+) combina varios errores en uno solo, útil para validar múltiples campos. Los errores originales siguen siendo detectables con errors.Is() y errors.As().
Error personalizado
type NotFoundError struct {
Nombre string
}
func (e *NotFoundError) Error() string {
return e.Nombre + " no encontrado"
}
// usar
return &NotFoundError{Nombre: "usuario"}Cualquier tipo con un método Error() string implementa la interface error. Los errores personalizados pueden transportar datos adicionales (como Nombre), útiles para el llamador.
panic y recover
func seguro() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recuperado:", r)
}
}()
panic("error fatal")
}
// panic interrumpe; recover solo en deferpanic() interrumpe el flujo y se propaga por la pila; recover(), llamado dentro de un defer, lo captura y recupera el control. Es último recurso — en Go, prefiere devolver un error.
Pacotes e Módulos
go.mod
module github.com/usuario/proyecto
go 1.22
require (
github.com/gorilla/mux v1.8.1
)El archivo go.mod define el módulo: la ruta (nombre) y la versión de Go. Se crea con go mod init. Las dependencias van en require y las versiones exactas en go.sum.
go get
# añadir dependencia go get github.com/gorilla/mux # versión específica go get github.com/gorilla/mux@v1.8.1 # actualizar go get -u ./... # quitar no usadas go mod tidy
go get descarga y añade dependencias al go.mod. Puedes fijar una versión con @v1.8.1 y actualizar todo con -u. go mod tidy limpia las que ya no se usan.
import
import (
"fmt"
"os"
"net/http"
alias "outro/paquete" // renombrar
_ "driver/mysql" // solo side-effects
. "math" // dot import (evitar)
)
fmt.Println("hola")import trae paquetes al archivo. Puedes dar un alias para evitar conflictos, usar _ para ejecutar solo la inicialización (drivers) o . para importar sin prefijo (desaconsejado).
go mod
# crear módulo go mod init github.com/user/proj # limpiar dependencias go mod tidy # descargar todas go mod download # verificar integridad go mod verify # ver el grafo de dependencias go mod graph
Los comandos go mod gestionan el módulo: init crea el go.mod, tidy sincroniza las dependencias con el código, download las guarda en la caché y verify confirma las sumas.
Exportación (mayúscula)
package calculo
// Mayúscula = exportado (público)
func Sumar(a, b int) int {
return a + b
}
type Servidor struct {
Host string // campo público
puerto int // campo privado
}
// minúscula = privado al paquete
func interno() { }En Go, la visibilidad es por capitalización: los nombres con mayúscula (Sumar, Servidor) se exportan y son accesibles fuera del paquete. Los nombres con minúscula son privados al paquete.
Organización del proyecto
meuprojeto/
├── go.mod
├── go.sum
├── main.go // package main
├── cmd/
│ └── server/
│ └── main.go // otro binario
├── internal/ // privado al módulo
│ └── handler/
└── pkg/ // reutilizable
└── util/Un proyecto Go se organiza por paquetes en carpetas. cmd/ guarda los binarios, internal/ código privado al módulo (el compilador impide el import externo) y pkg/ código reutilizable.
init()
package config
var baseURL string
func init() {
// se ejecuta antes de main()
baseURL = os.Getenv("API_URL")
if baseURL == "" {
baseURL = "http://localhost"
}
}La función init() se ejecuta automáticamente antes de main(), una vez por paquete. Sirve para inicializar estado, registrar drivers o validar configuración. Puede haber varias por archivo.
package main
package main
import "fmt"
// punto de entrada obligatorio
func main() {
fmt.Println("Hola, mundo!")
}Un programa ejecutable necesita package main y una función main() sin parámetros ni retorno. Es el punto de entrada. Los paquetes de biblioteca usan otros nombres.
Dicas e CLI
CLI: comandos esenciales
go run main.go # compila y ejecuta go build # genera binario go install # compila e instala go test ./... # corre tests go fmt ./... # formatea código go mod tidy # limpia dependencias go clean # limpia cachés
Go tiene una única herramienta, go. run compila y ejecuta, build genera el binario, test corre los tests y fmt formatea el código con el estilo estándar. ./... abarca todos los paquetes.
Interfaces útiles
// fmt.Stringer
type Ponto struct{ X, Y int }
func (p Ponto) String() string {
return fmt.Sprintf("(%d,%d)", p.X, p.Y)
}
// io.Reader / io.Writer
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}La biblioteca estándar define interfaces pequeñas y ubicuas. fmt.Stringer (String()) personaliza la impresión. io.Reader e io.Writer abstraen cualquier fuente o destino de datos.
Buenas prácticas
// 1. Tratar siempre los errores
if err != nil {
return err
}
// 2. Nombres cortos y claros
i, v := range s // no "index, value"
// 3. Composición en vez de herencia
// 4. Receptor corto
func (s *Servidor) Start() { }
// 5. Context como 1.er parámetro
func Get(ctx context.Context, id string) { }Go valora la simplicidad: trata siempre los errores, usa nombres cortos, prefiere la composición a la herencia y pasa context.Context como primer parámetro. Formatea siempre con go fmt.
Tests (testing)
// suma_test.go
package calculo
import "testing"
func TestSomar(t *testing.T) {
got := Sumar(2, 3)
want := 5
if got != want {
t.Errorf("Sumar(2,3) = %d; quero %d", got, want)
}
}Los tests viven en archivos _test.go con funciones TestX(t *testing.T). Se ejecutan con go test. t.Errorf() registra un fallo sin parar; t.Fatalf() detiene el test.
io.Reader en la práctica
import (
"io"
"os"
"strings"
)
r := strings.NewReader("hola mundo")
// copiar de un Reader a un Writer
io.Copy(os.Stdout, r)
// leer todo
data, _ := io.ReadAll(r)
// un archivo es un Reader
f, _ := os.Open("f.txt")
io.Copy(os.Stdout, f)io.Reader es la abstracción central de entrada. io.Copy() transfiere datos de un Reader a un Writer de forma eficiente. Los archivos, strings y respuestas HTTP son todos Reader.
Table-driven tests
func TestSomar(t *testing.T) {
pruebas := []struct {
a, b, want int
}{
{1, 2, 3},
{0, 0, 0},
{-1, 1, 0},
}
for _, tt := range pruebas {
if got := Sumar(tt.a, tt.b); got != tt.want {
t.Errorf("Sumar(%d,%d) = %d; quero %d",
tt.a, tt.b, got, tt.want)
}
}
}Los table-driven tests definen varios casos en un slice de structs e iteran sobre ellos con range. Es el patrón idiomático en Go: compacto, fácil de extender y con mensajes de error claros.
JSON
type Persona struct {
Nombre string `json:"nombre"`
Edad int `json:"edad"`
}
// struct -> JSON
data, _ := json.Marshal(Persona{"Ana", 30})
// JSON -> struct
var p Persona
json.Unmarshal(data, &p)
// con formateo
json.MarshalIndent(p, "", " ")El paquete encoding/json serializa structs. Marshal() convierte a JSON y Unmarshal() hace lo inverso. Las tags json:"nombre" definen los nombres de los campos en el JSON.
go vet y formateo
# análisis estático go vet ./... # formatear go fmt ./... # formatear + organizar imports goimports -w . # linter completo (externo) golangci-lint run
go vet detecta errores sospechosos que compilan pero están mal. go fmt formatea el código con el estilo oficial. goimports también organiza los imports automáticamente.
Generics (Go 1.18+)
// type parameter
func Max[T constraints.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
Max(3, 7) // 7
Max("a", "b") // "b"
// slice genérico
func Map[T, U any](s []T, f func(T) U) []U {
res := make([]U, len(s))
for i, v := range s {
res[i] = f(v)
}
return res
}Los generics permiten funciones y tipos parametrizados por tipo, con [T ...]. La restricción constraints.Ordered limita T a tipos ordenables. Reduce el código duplicado manteniendo la seguridad de tipos.