Primeros Pasos
Hola Mundo
Todo programa Go empieza en la función main() del package main. Usa 'go run' para ejecutar, 'go build' para compilar. gofmt formatea automáticamente el código (tabuladores, espaciado). La sentencia import trae paquetes: fmt maneja E/S formateada.
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
fmt.Printf("Name: %s, Age: %d\n", "Alice", 30)
}
// Run: go run main.go
// Build: go build -o app main.go
// Format: gofmt -w main.goGo Modules (go.mod)
Los módulos de Go (go.mod) gestionan dependencias desde Go 1.11. 'go mod init' crea el archivo de módulo. 'go get' añade dependencias. 'go mod tidy' elimina imports no usados y añade los faltantes. La ruta del módulo es la ruta de import para tu paquete.
// Initialize a new module
// $ go mod init github.com/user/project
// go.mod file:
module github.com/user/project
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
golang.org/x/sync v0.3.0
)
// Add dependencies:
// $ go get github.com/gin-gonic/gin
// $ go mod tidy // clean up unused depsEstructura de Paquetes
Go organiza el código en paquetes: un paquete por directorio. Los nombres capitalizados (Println, Add) son exportados (públicos); los minúsculas (privados) son privados del paquete. El directorio 'internal' restringe los imports al módulo padre. El nombre del paquete debería coincidir con el nombre del directorio.
// project structure:
// myproject/
// go.mod
// main.go // package main
// utils/
// helper.go // package utils
// math.go
// internal/
// api/ // private (only importable within parent)
// main.go
package main
import (
"fmt"
"github.com/user/project/utils"
)
func main() {
fmt.Println(utils.Add(1, 2))
}
// Exported names start with uppercase letter
// unexported names start with lowercase letterVariables y Constantes
Usa 'var name type = value' para declaraciones explícitas, 'name := value' para declaraciones cortas (solo funciones). Go tiene valores cero: 0 para números, '' para cadenas, false para booleanos, nil para punteros/slices/maps. Las constantes (const) son en tiempo de compilación y no pueden usar :=.
package main
import "fmt"
// Package-level declarations
var version = "1.0.0" // inferred type
const Pi = 3.14159
func main() {
// var with type
var name string = "Alice"
var age int = 30
// Short declaration (only inside functions)
city := "NYC"
// Multiple assignment
a, b, c := 1, 2, 3
// Zero values (default)
var x int // 0
var s string // ""
var ok bool // false
var p *int // nil
fmt.Println(name, age, city, a, b, c, x, s, ok, p)
}Tipos Básicos y Conversión
Go requiere conversión explícita de tipo: no hay conversión implícita como en C. rune es un alias de int32 (representa un code point Unicode). byte es un alias de uint8. Convertir entre tipos numéricos puede perder precisión (float a int trunca). string(65) convierte un code point a su carácter.
// Numeric types
var i int = 42
var i8 int8 = 127
var u uint = 42
var f32 float32 = 3.14
var f64 float64 = 3.141592653589793
// Other types
var b bool = true
var s string = "hello"
var r rune = 'A' // int32 alias for Unicode code point
var by byte = 255 // uint8 alias
// Type conversion (explicit, no implicit conversion)
var n int = 42
var f float64 = float64(n) // int -> float64
var s string = string(65) // 65 -> "A" (rune to string)
var i2 int = int(f) // float64 -> int (truncates)
// Type inference
var x = 42 // int
var y = 3.14 // float64
var z = len("hi") // intCadenas y Formateo
Fundamentos de Cadenas
Las cadenas de Go son secuencias de bytes inmutables. len() devuelve el conteo de bytes, no de caracteres: usa utf8.RuneCountInString() para Unicode. Las cadenas se comparan lexicográficamente con ==, <, >. Range sobre una cadena itera por rune (code point Unicode), manejando correctamente los caracteres multibyte.
package main
import (
"fmt"
"strings"
)
func main() {
s := "Hello, World"
// Length (bytes, not runes!)
fmt.Println(len(s)) // 12
// Concatenation
s1 := "Hello" + " " + "World"
s2 := fmt.Sprintf("%s = %d", "age", 30)
// Comparison
fmt.Println("abc" == "abc") // true
fmt.Println("a" < "b") // true (lexicographic)
// Iteration (by rune)
for i, r := range "Go语" {
fmt.Printf("%d: %c\n", i, r)
}
}Paquete strings
El paquete strings proporciona operaciones comunes de cadena. Contains/HasPrefix/HasSuffix comprueban subcadenas. Split divide por delimitador; Join combina. Replace toma un conteo (-1 para todas). TrimSpace elimina espacios en blanco iniciales/finales. Todas las funciones devuelven cadenas nuevas (las cadenas son inmutables).
import (
"fmt"
"strings"
)
s := "Hello, World"
// Case
strings.ToUpper(s) // "HELLO, WORLD"
strings.ToLower(s) // "hello, world"
strings.Title("hello world") // "Hello World"
// Search
strings.Contains(s, "World") // true
strings.HasPrefix(s, "Hello") // true
strings.HasSuffix(s, "World") // true
strings.Index(s, "World") // 7 (-1 if not found)
strings.Count(s, "l") // 3
// Split & Join
parts := strings.Split("a,b,c", ",") // ["a", "b", "c"]
joined := strings.Join(parts, "-") // "a-b-c"
// Replace
strings.Replace(s, "l", "L", 1) // "HeLlo, World" (1 occurrence)
strings.ReplaceAll(s, "l", "L") // "HeLLo, WorLd"
strings.TrimSpace(" hi ") // "hi"strconv y Formateo
strconv convierte entre cadenas y números: comprueba siempre el error de retorno. fmt.Printf formatea la salida: %d (int), %f (float), %s (cadena), %t (bool), %T (tipo), %x (hex), %q (entre comillas). Usa %.2f para 2 decimales, %05d para rellenar con ceros. Sprintf devuelve la cadena en lugar de imprimirla.
import (
"fmt"
"strconv"
)
// String to number
n, err := strconv.Atoi("42") // string -> int
f, err := strconv.ParseFloat("3.14", 64)
// Number to string
s1 := strconv.Itoa(42) // int -> string
s2 := strconv.FormatFloat(3.14, 'f', 2, 64) // "3.14"
// Printf formatting
fmt.Printf("Int: %d\n", 42)
fmt.Printf("Float: %.2f\n", 3.14159)
fmt.Printf("String: %s\n", "hi")
fmt.Printf("Bool: %t\n", true)
fmt.Printf("Type: %T\n", 42) // int
fmt.Printf("Hex: %x\n", 255) // ff
fmt.Printf("Pad: %05d\n", 42) // 00042
fmt.Printf("Quote: %q\n", "hi") // "hi"
// Sprintf returns formatted string
result := fmt.Sprintf("Name: %s, Age: %d", "Alice", 30)Cadenas Raw y Multilínea
Las cadenas raw (backticks) preservan todo literalmente: sin secuencias de escape, pueden ocupar múltiples líneas. Úsalas para regex, SQL, plantillas HTML. Las cadenas interpretadas (comillas dobles) procesan \n, \t, etc. Para concatenación eficiente de cadenas en bucles, usa strings.Builder (evita asignación O(n²)).
// Raw string literal (backticks) - no escape processing
raw := `This is a
multiline string
with \n (literal backslash-n)`
// Interpreted string (double quotes) - processes escapes
interpreted := "Line1\nLine2\tTabbed"
// Raw strings are useful for:
// - Regex patterns
// - HTML/SQL templates
// - File paths on Windows
regex := `^d{4}-d{2}-d{2}$`
// String builder (efficient concatenation)
var sb strings.Builder
for i := 0; i < 1000; i++ {
sb.WriteString("line\n")
}
result := sb.String()Unicode y Runes
Las cadenas de Go son secuencias de bytes codificadas en UTF-8. len() da bytes; utf8.RuneCountInString() da caracteres. Range sobre una cadena decodifica UTF-8 automáticamente. Para modificar una cadena, conviértela a []rune, cámbiala y conviértela de vuelta. Esto es esencial para el procesamiento de texto internacionalizado.
import (
"fmt"
"unicode/utf8"
)
s := "Hello, 世界"
// Byte length vs rune length
fmt.Println(len(s)) // 13 (bytes)
fmt.Println(utf8.RuneCountInString(s)) // 9 (runes/characters)
// Decode runes manually
for i := 0; i < len(s); {
r, size := utf8.DecodeRuneInString(s[i:])
fmt.Printf("%c ", r)
i += size
}
// Range automatically decodes runes
for i, r := range s {
fmt.Printf("%d:%c ", i, r)
}
// Rune slice (mutable)
runes := []rune("Hello")
runes[0] = 'J'
s2 := string(runes) // "Jello"Flujo de Control
If / Else
El if de Go no necesita paréntesis alrededor de las condiciones, pero las llaves son obligatorias (incluso para cuerpos de una sola línea). La sentencia init (if x := f(); x > 0) tiene ámbito limitado al bloque if/else: común para comprobación de errores. Este patrón mantiene el ámbito de variables ajustado.
// Basic if/else
score := 85
if score >= 90 {
fmt.Println("A")
} else if score >= 80 {
fmt.Println("B")
} else {
fmt.Println("C")
}
// If with initialization statement
if n := computeValue(); n > 100 {
fmt.Println("big:", n)
} else {
fmt.Println("small:", n)
}
// n is not accessible here (scoped to if)
// No parentheses needed, but braces are requiredBucles for
Go tiene solo una palabra clave de bucle: 'for'. Maneja estilo C, estilo while (for cond), infinito (for) e iteración (for range). Range funciona con slices, maps, cadenas y canales. Usa _ para saltar el índice o valor. El orden de iteración de maps es aleatorio por diseño.
// C-style for
for i := 0; i < 5; i++ {
fmt.Println(i)
}
// While-style (condition only)
n := 10
for n > 0 {
n--
}
// Infinite loop
for {
break
}
// Range (iterate slices, maps, strings)
nums := []int{10, 20, 30}
for index, value := range nums {
fmt.Printf("%d: %d\n", index, value)
}
// Skip index or value with _
for _, value := range nums {
fmt.Println(value)
}
// Range over map (unordered!)
m := map[string]int{"a": 1, "b": 2}
for key, val := range m {
fmt.Println(key, val)
}Switch
El switch de Go no cae por defecto (a diferencia de C/Java): cada case es una rama separada. Usa fallthrough para forzarlo. Múltiples valores por case usan comas. Switch sin expresión actúa como una cadena if/else más limpia. Switch con sentencia init limita la variable al switch.
// Basic switch
day := 3
switch day {
case 1:
fmt.Println("Mon")
case 2, 3, 4, 5:
fmt.Println("Weekday")
case 6, 7:
fmt.Println("Weekend")
default:
fmt.Println("Invalid")
}
// Switch with no expression (like if/else chain)
switch {
case score >= 90:
grade = "A"
case score >= 80:
grade = "B"
default:
grade = "C"
}
// Switch with init
switch os := runtime.GOOS; os {
case "linux":
fmt.Println("Linux")
case "darwin":
fmt.Println("macOS")
}
// Fallthrough (rare, goes to next case unconditionally)
switch 1 {
case 1:
fmt.Println("one")
fallthrough
case 2:
fmt.Println("two") // executes even though value is 1
}Defer
defer programa una llamada a función para que se ejecute cuando la función envolvente retorne: último en entrar, primero en salir (LIFO). Úsalo para limpieza (cerrar archivos, liberar locks, cerrar conexiones). Los argumentos de las llamadas diferidas se evalúan inmediatamente, pero la llamada se ejecuta al retornar. Los defers se ejecutan incluso si la función entra en pánico.
// Defer runs when function returns (LIFO order)
func main() {
defer fmt.Println("third")
defer fmt.Println("second")
fmt.Println("first")
}
// Output: first, second, third
// Common: resource cleanup
func readFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // runs when readFile returns
// ... use f ...
return nil
}
// Defer evaluates arguments immediately
i := 1
defer fmt.Println(i) // prints 1 (not 2)
i = 2Goto, Break, Continue
break sale del bucle más interno; continue salta a la siguiente iteración. Las etiquetas (break outer) salen de bucles anidados. goto existe pero rara vez se usa: prefiere flujo de control estructurado. Las etiquetas también pueden usarse con continue para saltar a la siguiente iteración de un bucle externo.
// Break and continue
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue // skip even numbers
}
if i > 7 {
break // exit loop
}
fmt.Println(i)
}
// Break with label (for nested loops)
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
break outer // exits both loops
}
}
}
// Goto (rarely used, avoid)
i := 0
loop:
if i < 5 {
fmt.Println(i)
i++
goto loop
}Select (Operaciones de Canal)
select es como switch para canales: espera múltiples operaciones de canal y elige la primera lista. El case por defecto lo hace no bloqueante. Usa select en un bucle para patrones orientados a eventos. time.After() crea un canal de timeout. select elige aleatoriamente si múltiples cases están listos, previniendo inanición.
// Select waits on multiple channel operations
select {
case msg := <-messages:
fmt.Println("received:", msg)
case <-timeout:
fmt.Println("timed out")
default:
fmt.Println("no activity") // non-blocking
}
// Select with send
ch1 := make(chan int)
ch2 := make(chan int)
go func() { ch1 <- 1 }()
go func() { ch2 <- 2 }()
select {
case v := <-ch1:
fmt.Println("ch1:", v)
case v := <-ch2:
fmt.Println("ch2:", v)
}
// Select in a loop (event loop pattern)
for {
select {
case msg := <-ch:
fmt.Println(msg)
case <-time.After(5 * time.Second):
return // timeout after 5s of inactivity
}
}Funciones
Definir y Múltiples Returns
Las funciones de Go pueden devolver múltiples valores: el patrón canónico es (result, error). Los returns con nombre actúan como variables declaradas inicializadas a valores cero; un return 'desnudo' los usa. Los returns con nombre mejoran la legibilidad para funciones complejas pero pueden confundir si se abusan. Comprueba siempre los errores inmediatamente.
// Function with multiple return values
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("divide by zero")
}
return a / b, nil
}
// Named return values
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return // "naked" return (uses named values)
}
// Usage
result, err := divide(10, 2)
if err != nil {
log.Fatal(err)
}
fmt.Println(result) // 5
x, y := split(100)
fmt.Println(x, y)Variádicas y Closures
Las funciones variádicas (...T) aceptan cualquier número de argumentos, recibidos como slice. Esparce un slice con .... Las closures capturan variables por referencia de su ámbito envolvente: la función counter() devuelve una closure que recuerda 'count'. Las closures son útiles para callbacks, iteradores y funciones con estado.
// Variadic function (variable arguments)
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(1, 2, 3, 4, 5)) // 15
// Spread a slice
nums := []int{1, 2, 3}
fmt.Println(sum(nums...)) // 6
// Closure (function that captures variables)
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
next := counter()
fmt.Println(next()) // 1
fmt.Println(next()) // 2
fmt.Println(next()) // 3Funciones como Valores
Las funciones en Go son valores de primera clase: pueden asignarse a variables, pasarse como argumentos y almacenarse en estructuras de datos. Define tipos de función con 'type Name func(params) returns'. Los maps de funciones son útiles para tablas de dispatch, handlers de comandos y patrones strategy.
// Functions are first-class values
func apply(f func(int) int, x int) int {
return f(x)
}
func double(x int) int { return x * 2 }
func square(x int) int { return x * x }
fmt.Println(apply(double, 5)) // 10
fmt.Println(apply(square, 5)) // 25
// Function type
type MathFunc func(int) int
var fn MathFunc = func(x int) int { return x + 1 }
fmt.Println(fn(10)) // 11
// Map of functions
ops := map[string]func(int, int) int{
"add": func(a, b int) int { return a + b },
"sub": func(a, b int) int { return a - b },
"mul": func(a, b int) int { return a * b },
}
fmt.Println(ops["add"](3, 4)) // 7init() y Funciones Anónimas
Las funciones init() se ejecutan automáticamente antes de main(), en el orden en que se declaran. Úsalas para setup (carga de config, validación, registro). Se permiten múltiples funciones init() por archivo. Las funciones anónimas pueden invocarse inmediatamente (IIFE) o asignarse a variables. También se usan para goroutines.
// init() runs before main(), once per package
// Multiple init() functions allowed, run in order declared
package main
import "fmt"
var config string
func init() {
// Setup code: config, connections, validation
config = "production"
fmt.Println("init 1")
}
func init() {
fmt.Println("init 2, config:", config)
}
func main() {
fmt.Println("main")
}
// Output: init 1, init 2, main
// Anonymous function (IIFE)
result := func(x int) int {
return x * 2
}(5) // immediately invoked, result = 10
// Goroutine with anonymous function
go func(msg string) {
fmt.Println(msg)
}("async")Genéricos (Go 1.18+)
Go 1.18+ admite genéricos con parámetros de tipo [T any]. Las restricciones de tipo (interfaces) limitan los tipos permitidos: usa 'any' para sin restricción, 'comparable' para operadores == / !=. Las restricciones de unión (int | float64) permiten tipos específicos. Los genéricos habilitan estructuras de datos y algoritmos reutilizables type-safe sin duplicación de código.
// Generic function with type parameter
func Map[T, U any](slice []T, f func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = f(v)
}
return result
}
nums := []int{1, 2, 3}
doubled := Map(nums, func(n int) int { return n * 2 })
// [2, 4, 6]
strs := Map(nums, func(n int) string {
return fmt.Sprintf("num%d", n)
})
// ["num1", "num2", "num3"]
// Type constraints
type Number interface {
int | int64 | float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
// comparable constraint (for maps/slices keys)
func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}Métodos
Los métodos son funciones con un argumento receptor. Los receptores por valor (r Rectangle) trabajan sobre una copia: no pueden modificar el original. Los receptores por puntero (r *Rectangle) pueden modificar y evitan copiar structs grandes. La consistencia importa: si un método usa receptor por puntero, todos deberían. Los métodos pueden definirse en cualquier tipo del mismo paquete.
type Rectangle struct {
Width, Height float64
}
// Method with value receiver
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// Method with pointer receiver (can modify)
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
// Method on non-struct types
type MyString string
func (s MyString) Shout() string {
return strings.ToUpper(string(s)) + "!"
}
// Usage
r := Rectangle{Width: 10, Height: 5}
fmt.Println(r.Area()) // 50
r.Scale(2)
fmt.Println(r.Width) // 20
s := MyString("hello")
fmt.Println(s.Shout()) // HELLO!Estructuras de Datos
Arrays y Slices
Los arrays tienen longitud fija; los slices son dinámicos (respaldados por arrays). append() añade elementos, aumentando la capacidad según sea necesario. make([]T, len, cap) pre-asigna para eficiencia. El slicing crea una vista (comparte el array subyacente): usa copy() para datos independientes. Comprueba siempre len/cap al optimizar.
// Array (fixed length, rarely used directly)
var arr [3]int = [3]int{1, 2, 3}
arr2 := [...]int{4, 5, 6} // size inferred
// Slice (dynamic array, most common)
nums := []int{1, 2, 3}
nums = append(nums, 4) // [1, 2, 3, 4]
nums = append(nums, 5, 6, 7) // [1, 2, 3, 4, 5, 6, 7]
// Make a slice with capacity
s := make([]int, 3, 10) // len=3, cap=10
// Slicing
sub := nums[1:4] // [2, 3, 4]
first := nums[:2] // [1, 2]
last := nums[3:] // [4, 5, 6, 7]
// Copy
dst := make([]int, len(nums))
copy(dst, nums)
// Length and capacity
fmt.Println(len(nums), cap(nums))Maps
Los maps son las tablas hash de Go: pares clave/valor no ordenados. El patrón comma-ok (val, ok := m[key]) comprueba si existe una clave. delete() elimina una clave. El orden de iteración de maps es aleatorio por diseño. Los maps son tipos referencia: pasarlos a funciones comparte los datos subyacentes. Los maps nil no pueden escribirse (usa make()).
// Create a map
m := map[string]int{
"Alice": 30,
"Bob": 25,
}
// Using make
ages := make(map[string]int)
ages["Charlie"] = 35
// Access
fmt.Println(m["Alice"]) // 30
// Check existence (comma-ok pattern)
age, ok := m["David"]
if !ok {
fmt.Println("not found")
}
// Delete
delete(m, "Bob")
// Iterate (random order)
for name, age := range m {
fmt.Printf("%s: %d\n", name, age)
}
// Length
fmt.Println(len(m))
// Nested maps
matrix := map[string]map[string]int{}
matrix["row1"] = map[string]int{"col1": 1}Structs
Los structs agrupan campos relacionados. Usa inicialización con nombre (Person{Name: ...}) para claridad. new() devuelve un puntero con valores cero. Los structs anónimos son útiles para formas de datos puntuales. El embedding de structs (sin nombre de campo) promueve los campos y métodos del struct embebido: la alternativa de Go a la herencia.
// Define a struct
type Person struct {
Name string
Age int
Address string // field names are exported (capitalized)
}
// Create instances
p1 := Person{Name: "Alice", Age: 30}
p2 := Person{"Bob", 25, "NYC"} // positional (not recommended)
p3 := new(Person) // returns *Person, zero values
p3.Name = "Charlie"
// Anonymous struct (one-off)
config := struct {
Port int
Host string
}{
Port: 8080,
Host: "localhost",
}
// Struct embedding (composition)
type Employee struct {
Person // embedded (promotes fields)
Salary float64
}
emp := Employee{
Person: Person{Name: "Dave", Age: 40},
Salary: 50000,
}
fmt.Println(emp.Name) // "Dave" (promoted from Person)Punteros
Los punteros de Go (*T) contienen direcciones de memoria. & toma la dirección, * desreferencia. A diferencia de C, Go no tiene aritmética de punteros (más seguro). Los punteros a struct permiten abreviatura (u.Name en lugar de (*u).Name). Los punteros nil causan pánico al desreferenciar. Go tiene garbage collection: no se necesita free() manual.
// Pointer basics
x := 42
p := &x // p is *int, points to x
fmt.Println(*p) // 42 (dereference)
*p = 100 // modify x through pointer
fmt.Println(x) // 100
// new() allocates and returns pointer
p2 := new(int) // *int, value 0
*p2 = 42
// Pointers to structs
type User struct{ Name string }
u := &User{Name: "Alice"} // *User
u.Name = "Bob" // (*u).Name shorthand
fmt.Println(u.Name) // Bob
// nil pointer
var p3 *int // nil
// *p3 // panic: nil pointer dereference
// No pointer arithmetic (unlike C)
// p++ is illegalOperaciones con Slices
Go no tiene filter/map/reduce integrados: escríbelos o usa el paquete slices (Go 1.21+). Insert/remove requieren desplazar elementos con append+copy. Ten cuidado con el aliasing de slices: s[:i] y s[i+1:] comparten el array subyacente. Para slices 2D, asigna cada fila por separado.
// Filter
func filter(nums []int, pred func(int) bool) []int {
result := []int{}
for _, n := range nums {
if pred(n) {
result = append(result, n)
}
}
return result
}
evens := filter([]int{1,2,3,4,5}, func(n int) bool {
return n%2 == 0
}) // [2, 4]
// Insert at index
func insert(s []int, i, v int) []int {
s = append(s, 0)
copy(s[i+1:], s[i:])
s[i] = v
return s
}
// Remove at index
func remove(s []int, i int) []int {
return append(s[:i], s[i+1:]...)
}
// 2D slice
matrix := make([][]int, 3)
for i := range matrix {
matrix[i] = make([]int, 4)
}Ordenación y Búsqueda
sort.Ints/Strings/Float64s ordenan in place. sort.Slice con un comparador maneja tipos personalizados. sort.Search* hace búsqueda binaria en slices ordenados. Go 1.21+ añade el paquete slices con funciones de ordenación genéricas. Toda la ordenación es in-place: haz una copia primero si necesitas el orden original.
import "sort"
// Sort a slice
nums := []int{3, 1, 4, 1, 5, 9, 2, 6}
sort.Ints(nums) // [1 1 2 3 4 5 6 9]
fmt.Println(nums)
// Sort strings
strs := []string{"banana", "apple", "cherry"}
sort.Strings(strs) // [apple banana cherry]
// Sort with custom comparator
type Person struct {
Name string
Age int
}
people := []Person{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
}
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age // by age ascending
})
// Binary search (sorted slice)
idx := sort.SearchInts(nums, 4) // index of 4
// Sort package (Go 1.21+): slices.Sort
import "slices"
slices.Sort(people) // needs Less method or cmp funcMétodos e Interfaces
Definir Interfaces
Las interfaces definen firmas de métodos. Go usa tipado estructural: un tipo implementa una interfaz automáticamente si tiene todos los métodos requeridos (sin declaración 'implements' explícita). Esto habilita diseño desacoplado: define interfaces donde las usas, no donde las implementas.
// Interface: a set of method signatures
type Shape interface {
Area() float64
Perimeter() float64
}
// Implement implicitly (no 'implements' keyword)
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}
// Use the interface
func describe(s Shape) {
fmt.Printf("Area: %.2f, Perimeter: %.2f\n",
s.Area(), s.Perimeter())
}
c := Circle{Radius: 5}
describe(c) // works! Circle implements ShapeInterfaz Vacía y Type Assertions
interface{} (o 'any' en Go 1.18+) contiene cualquier valor. La type assertion (v.(T)) extrae el tipo concreto: entra en pánico si el tipo es incorrecto, así que usa el patrón comma-ok. El type switch (switch v.(type)) maneja múltiples tipos de forma limpia. La interfaz vacía es útil para contenedores genéricos pero pierde type safety: prefiere genéricos.
// Empty interface (any type)
// Go 1.18+: 'any' is an alias for interface{}
func printAny(v any) {
fmt.Println(v)
}
printAny(42)
printAny("hello")
printAny([]int{1, 2, 3})
// Type assertion
var i any = "hello"
s := i.(string) // panics if not string
fmt.Println(s)
// Safe type assertion (comma-ok)
s, ok := i.(string)
if ok {
fmt.Println("string:", s)
}
// Type switch
func describe(v any) {
switch x := v.(type) {
case int:
fmt.Printf("int: %d\n", x)
case string:
fmt.Printf("string: %s\n", x)
case []int:
fmt.Printf("int slice: %v\n", x)
default:
fmt.Printf("unknown type: %T\n", x)
}
}Composición de Interfaces
Las interfaces pueden embeber otras interfaces (composición). io.Reader e io.Writer son las interfaces más importantes de Go: implementadas por archivos, conexiones de red, búferes, etc. Esto habilita abstracciones potentes: funciones que toman io.Reader funcionan con cualquier fuente legible. Las interfaces pequeñas y enfocadas (1-3 métodos) son idiomáticas.
// Compose interfaces
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// ReadWriter combines Reader and Writer
type ReadWriter interface {
Reader
Writer
}
// io.Reader and io.Writer are built-in interfaces
// Many types implement them: *os.File, *bytes.Buffer, net.Conn
func copyData(r io.Reader, w io.Writer) error {
buf := make([]byte, 1024)
for {
n, err := r.Read(buf)
if n > 0 {
if _, err := w.Write(buf[:n]); err != nil {
return err
}
}
if err == io.EOF {
break
}
if err != nil {
return err
}
}
return nil
}Receptor Puntero vs Valor
Los receptores por puntero pueden modificar el struct y evitar copiar valores grandes. Los receptores por valor son seguros (no pueden modificar) y permiten el método tanto en valores como en punteros. Si algún método tiene receptor por puntero, todos deberían (por consistencia). Los métodos con receptor por puntero satisfacen interfaces tanto para T como para *T; los de receptor por valor solo para T.
type Counter struct {
count int
}
// Value receiver: works on a copy, can't modify
func (c Counter) Get() int {
return c.count
}
// Pointer receiver: can modify the original
func (c *Counter) Increment() {
c.count++
}
c := Counter{}
c.Increment() // Go auto-takes address (&c).Increment()
c.Increment()
fmt.Println(c.Get()) // 2
// Interface implementation note:
// *Counter implements both Get() and Increment()
// Counter (value) implements only Get()
// So *Counter satisfies an interface requiring bothInterfaz Stringer
La interfaz Stringer (String() string) controla cómo aparece un tipo en Print/Printf: como toString() en Java/JS. %v usa String(); %+v muestra nombres de campos; %#v muestra sintaxis Go. La interfaz error funciona igual: implementa Error() string para hacer cualquier tipo un error. Estas son las interfaces integradas más comunes de Go.
// Stringer interface (like toString() in other languages)
type Stringer interface {
String() string
}
type Person struct {
Name string
Age int
}
// Implement Stringer
func (p Person) String() string {
return fmt.Sprintf("%s (%d years)", p.Name, p.Age)
}
p := Person{Name: "Alice", Age: 30}
fmt.Println(p) // Alice (30 years) — uses String()
fmt.Printf("%v\n", p) // Alice (30 years)
fmt.Printf("%+v\n", p) // Name:Alice Age:30
fmt.Printf("%#v\n", p) // main.Person{Name:"Alice", Age:30}
// Error interface is similar:
// type error interface { Error() string }
// Implement Error() to make a type usable as an errorEmbedding de Tipos y Composición
Go usa composición en lugar de herencia. Embeber un struct promueve sus campos y métodos al struct externo. Puedes sobrescribir métodos promovidos definiendo un método con el mismo nombre en el tipo externo. Embeber una interfaz permite decorar/delegar: el tipo externo satisface la interfaz y puede reenviar llamadas.
// Embed a struct (composition over inheritance)
type Animal struct {
Name string
}
func (a Animal) Speak() string {
return a.Name + " makes a sound"
}
type Dog struct {
Animal // embedded — promotes Name and Speak()
Breed string
}
d := Dog{
Animal: Animal{Name: "Rex"},
Breed: "Labrador",
}
fmt.Println(d.Name) // "Rex" (promoted)
fmt.Println(d.Speak()) // "Rex makes a sound" (promoted)
fmt.Println(d.Animal.Name) // explicit access also works
// Override a promoted method
func (d Dog) Speak() string {
return d.Name + " barks!"
}
fmt.Println(d.Speak()) // "Rex barks!"
// Embed an interface
type Logger struct {
io.Writer // embed interface
}
// Logger now has Write() method, delegating to the embedded WriterManejo de Errores
Fundamentos de Errores
Go maneja errores como valores, no excepciones. Las funciones devuelven (result, error): comprueba siempre err != nil inmediatamente. errors.New() crea errores simples; fmt.Errorf() añade formateo. Los errores centinela (var ErrX = errors.New()) habilitan comparación con ==. Nunca ignores errores (usa _ solo cuando sea intencional).
// Functions return errors as last return value
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("cannot divide by zero")
}
return a / b, nil
}
// Always check errors immediately
result, err := divide(10, 0)
if err != nil {
log.Fatal(err) // or handle gracefully
}
fmt.Println(result)
// fmt.Errorf for formatted errors
func validate(age int) error {
if age < 0 {
return fmt.Errorf("invalid age: %d (must be positive)", age)
}
return nil
}
// Sentinel errors
var ErrNotFound = errors.New("not found")
if err := lookup("key"); err == ErrNotFound {
// handle not found
}Tipos de Error Personalizados
Los tipos de error personalizados implementan la interfaz error (Error() string). Transportan datos estructurados (campos, códigos, contexto) más allá de un simple mensaje. Usa type assertion (*ValidationError) para acceder a los datos. Es esencial para manejo de errores específico del dominio: por ejemplo, códigos de estado HTTP, detalles de validación, lógica de reintentos.
// Custom error type (implements error interface)
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error on '%s': %s", e.Field, e.Message)
}
// Use the custom error
func validateUser(name string) error {
if name == "" {
return &ValidationError{
Field: "name",
Message: "cannot be empty",
}
}
return nil
}
// Type-assert to access fields
err := validateUser("")
if ve, ok := err.(*ValidationError); ok {
fmt.Println("Field:", ve.Field) // "name"
fmt.Println("Message:", ve.Message) // "cannot be empty"
}errors.Is y errors.As (Go 1.13+)
Go 1.13+ añadió wrapping de errores con %w (fmt.Errorf). errors.Is() comprueba si un error coincide con un centinela (desenvolviendo la cadena). errors.As() extrae un tipo de error específico de la cadena. Esto habilita manejo de errores por capas: errores de bajo nivel envueltos con contexto, comprobados en niveles altos. Envuelve siempre con %w (no %v) para preservar la cadena.
import "errors"
// Wrapped errors with %w
var ErrNotFound = errors.New("not found")
func getUser(id int) error {
if id == 0 {
return fmt.Errorf("getUser(%d): %w", id, ErrNotFound)
}
return nil
}
// errors.Is: check if error matches (unwraps the chain)
err := getUser(0)
if errors.Is(err, ErrNotFound) {
fmt.Println("user not found")
}
// errors.As: extract a specific error type
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println("field:", ve.Field)
}
// errors.Unwrap: get the wrapped error
inner := errors.Unwrap(err)Panic y Recover
panic() es para errores irrecuperables (bugs, violaciones de invariantes): no para manejo de errores normal. recover() captura panics pero solo funciona en funciones diferidas. Usa panic/recover para: errores de programación (índice fuera de rango), fallos de inicialización de paquetes y proteger goroutines de colgar el programa. Prefiere devolver errores para fallos esperados.
// Panic: unrecoverable error (like throw)
func mustParse(s string) int {
n, err := strconv.Atoi(s)
if err != nil {
panic(fmt.Sprintf("invalid number: %s", s))
}
return n
}
// Recover: catch a panic (only in deferred functions)
func safeCall() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
panic("something went wrong")
}
// Practical: safe goroutine
func safeGo(fn func()) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Println("goroutine panic:", r)
}
}()
fn()
}()
}Patrones de Wrapping de Errores
Envuelve errores con contexto en cada capa usando fmt.Errorf con %w. Esto crea una cadena de errores: el nivel superior ve la ruta completa (getUserProfile → fetchUser → sql error). El contexto (nombre de función, parámetros) ayuda a depurar. Usa errors.Is/As para comprobar errores específicos en la cadena. Evita envolver el mismo error múltiples veces con el mismo contexto.
// Wrap errors with context as they propagate up
func fetchUser(id int) (*User, error) {
row := db.QueryRow("SELECT ... WHERE id = ?", id)
var u User
if err := row.Scan(&u.Name, &u.Age); err != nil {
return nil, fmt.Errorf("fetchUser(%d): %w", id, err)
}
return &u, nil
}
func getUserProfile(id int) (*Profile, error) {
user, err := fetchUser(id)
if err != nil {
return nil, fmt.Errorf("getUserProfile(%d): %w", id, err)
}
// ...
}
// At the top level, log the full chain
profile, err := getUserProfile(42)
if err != nil {
log.Printf("error: %v", err)
// Output: getUserProfile(42): fetchUser(42): sql: no rows
}
// golang.org/x/xerrors or pkg/errors for stack tracesDefer, Panic, Recover Juntos
Combina defer (limpieza), panic (errores fatales) y recover (capturar panics) para gestión robusta de recursos. Los defers se ejecutan en orden LIFO, incluso durante panics. El valor de retorno con nombre (err error) puede establecerse dentro de un recover diferido. Este patrón garantiza que archivos/conexiones se cierren y las transacciones se reviertan, incluso si el código entra en pánico.
// Complete pattern: cleanup + panic recovery
func processFile(path string) (err error) {
file, e := os.Open(path)
if e != nil {
return e
}
defer func() {
// Close file regardless of panic
file.Close()
// Recover from panic and convert to error
if r := recover(); r != nil {
err = fmt.Errorf("panic in processFile: %v", r)
}
}()
// Do work that might panic
data := parseFile(file) // might panic
return saveData(data)
}
// Defer for resource cleanup (always runs)
func withDatabase(db *sql.DB, fn func(*sql.DB) error) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer func() {
if err != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
return fn(tx)
}Concurrencia
Goroutines
Las goroutines son los hilos ligeros de Go: se inician con 'go'. Son baratas (~2KB de stack) y gestionadas por el scheduler del runtime de Go (planificación M:N). La función main no espera a las goroutines: usa sync.WaitGroup o canales para sincronización. Nunca uses time.Sleep para sincronización en producción (usa WaitGroup).
package main
import (
"fmt"
"time"
)
func sayHello(name string) {
for i := 0; i < 3; i++ {
fmt.Println(name, i)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
// Launch goroutine with 'go' keyword
go sayHello("Alice")
go sayHello("Bob")
// Anonymous goroutine
go func() {
fmt.Println("anonymous goroutine")
}()
// Wait for goroutines (simple approach)
time.Sleep(1 * time.Second)
fmt.Println("done")
}
// Goroutines are lightweight (~2KB stack, grows as needed)
// Millions of goroutines can run concurrentlyCanales
Los canales son conductos tipados para comunicación entre goroutines. Los sin búfer (make(chan T)) bloquean hasta que emisor y receptor están listos (síncrono). Los con búfer (make(chan T, n)) bloquean solo cuando están llenos (asíncrono). El emisor debería cerrar los canales, nunca el receptor. Range sobre un canal hasta que se cierra. Los canales habilitan 'compartir memoria comunicando'.
// Unbuffered channel (synchronous)
ch := make(chan string)
// Send and receive
go func() {
ch <- "hello" // blocks until received
}()
msg := <-ch // blocks until sent
fmt.Println(msg)
// Buffered channel (asynchronous)
buf := make(chan int, 3)
buf <- 1 // doesn't block (buffer has space)
buf <- 2
buf <- 3
// buf <- 4 // would block (buffer full)
fmt.Println(<-buf) // 1
// Close a channel (sender closes, never receiver)
close(buf)
// Range over channel (until closed)
for v := range buf {
fmt.Println(v)
}
// Check if closed
v, ok := <-buf
if !ok {
fmt.Println("channel closed")
}Sentencia Select
select permite a una goroutine esperar múltiples operaciones de canal: elige la primera lista (aleatorio si varias). El case por defecto lo hace no bloqueante. time.After() crea canales de timeout. Select es el corazón del Go concurrente: event loops, fan-in/fan-out, timeouts. Incluye siempre timeouts para evitar deadlocks.
// Select: multiplex channel operations
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "one"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "two"
}()
// Wait for first to arrive
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println("received:", msg1)
case msg2 := <-ch2:
fmt.Println("received:", msg2)
}
}
// Timeout with select
select {
case result := <-slowOperation():
fmt.Println(result)
case <-time.After(3 * time.Second):
fmt.Println("timeout!")
}
// Non-blocking receive (default case)
select {
case msg := <-ch:
fmt.Println(msg)
default:
fmt.Println("no message") // runs if no data
}sync.WaitGroup
sync.WaitGroup espera a que un grupo de goroutines termine. Add(n) incrementa el contador, Done() lo decrementa (usa defer), Wait() bloquea hasta cero. Pasa siempre las variables de bucle como parámetros a las goroutines para evitar bugs de captura de closures (arreglado en Go 1.22 pero aún recomendado). WaitGroup es más simple que los canales para concurrencia fire-and-forget.
import "sync"
func main() {
var wg sync.WaitGroup
// Launch 5 goroutines
for i := 0; i < 5; i++ {
wg.Add(1) // increment counter
go func(id int) {
defer wg.Done() // decrement when done
fmt.Printf("Worker %d started\n", id)
time.Sleep(time.Duration(id) * 100 * time.Millisecond)
fmt.Printf("Worker %d done\n", id)
}(i)
}
wg.Wait() // block until all Done() called
fmt.Println("all workers finished")
}
// Common mistake: passing loop variable to goroutine
// Always pass as parameter: go func(i int) { ... }(i)
// (Go 1.22+ fixes loop variable scoping, but still good practice)Mutex y Sync
sync.Mutex protege el estado compartido del acceso concurrente: Lock/Unlock con defer. RWMutex permite múltiples lectores o un escritor (mejor para cargas con muchas lecturas). sync.Once garantiza que la inicialización ocurra exactamente una vez (patrón singleton). Prefiere canales para comunicación, mutexes para proteger estado compartido. 'Comparte memoria comunicando; no comuniques compartiendo memoria.'
import "sync"
// Mutex: mutual exclusion lock
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock() // always unlock with defer
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
// RWMutex: multiple readers, one writer
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) (string, bool) {
c.mu.RLock() // read lock (multiple allowed)
defer c.mu.RUnlock()
val, ok := c.data[key]
return val, ok
}
func (c *Cache) Set(key, val string) {
c.mu.Lock() // write lock (exclusive)
defer c.mu.Unlock()
c.data[key] = val
}
// sync.Once: run exactly once
var (
once sync.Once
instance *Database
)
func GetDB() *Database {
once.Do(func() {
instance = &Database{}
})
return instance
}Patrones de Concurrencia
Worker pool: un número fijo de goroutines procesan jobs de un canal, enviando resultados a otro. Esto limita la concurrencia y previene el agotamiento de recursos. Fan-out/fan-in: distribuye trabajo entre goroutines, luego fusiona resultados. La idea clave: cierra los canales cuando no se enviarán más datos, para que los bucles range terminen. Estos patrones son la base del Go concurrente.
// Worker pool
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
time.Sleep(time.Second) // simulate work
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// Start 3 workers
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// Send 5 jobs
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
// Collect results
for a := 1; a <= 5; a++ {
fmt.Println(<-results)
}
}
// Fan-out/Fan-in
func fanOut(input <-chan int, workers int) <-chan int {
output := make(chan int)
var wg sync.WaitGroup
wg.Add(workers)
for i := 0; i < workers; i++ {
go func() {
defer wg.Done()
for v := range input {
output <- v * 2
}
}()
}
go func() { wg.Wait(); close(output) }()
return output
}E/S de Archivos y OS
Leer Archivos
os.ReadFile() lee un archivo entero en memoria (simple pero no para archivos grandes). Para archivos grandes, abre con os.Open(), lee en chunks, y siempre defer Close(). bufio.Scanner lee línea a línea: ideal para procesamiento de texto. Comprueba siempre los errores, especialmente io.EOF para saber cuándo se termina la lectura.
import (
"io"
"os"
)
// Read entire file (small files)
data, err := os.ReadFile("input.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
// Open and read (large files)
file, err := os.Open("large.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
// Read in chunks
buf := make([]byte, 1024)
for {
n, err := file.Read(buf)
if n > 0 {
// process buf[:n]
}
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
}
// Read line by line
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
fmt.Println(line)
}Escribir Archivos
os.WriteFile() crea/trunca y escribe atómicamente (simple). os.OpenFile() con flags da control: O_APPEND (añadir al final), O_CREATE (crear si falta), O_TRUNC (truncar). Usa bufio.Writer para muchas escrituras pequeñas (búfer en memoria, flush al final). El modo de archivo 0644: el propietario puede leer/escribir, otros pueden leer.
// Write entire file (creates or truncates)
err := os.WriteFile("output.txt", []byte("Hello, World!"), 0644)
if err != nil {
log.Fatal(err)
}
// Open for writing (with options)
file, err := os.OpenFile("log.txt",
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatal(err)
}
defer file.Close()
// Write
if _, err := file.WriteString("log entry\n"); err != nil {
log.Fatal(err)
}
// Buffered writer (efficient for many small writes)
writer := bufio.NewWriter(file)
for i := 0; i < 1000; i++ {
writer.WriteString(fmt.Sprintf("line %d\n", i))
}
writer.Flush() // don't forget to flush!
// File modes: 0644 = owner read/write, others read
// os.O_APPEND, O_CREATE, O_WRONLY, O_RDONLY, O_RDWR, O_TRUNCOperaciones de Archivo
os.Mkdir/MkdirAll crean directorios. os.ReadDir lista entradas eficientemente. filepath.WalkDir recorre directorios recursivamente. os.Stat da información del archivo (tamaño, mod time, permisos). os.IsNotExist comprueba si falta un archivo. os.Remove elimina un archivo; RemoveAll elimina directorios recursivamente. Comprueba siempre los errores.
// Create directory
os.Mkdir("mydir", 0755) // one level
os.MkdirAll("a/b/c", 0755) // nested directories
// List directory
entries, err := os.ReadDir(".")
if err != nil {
log.Fatal(err)
}
for _, entry := range entries {
fmt.Println(entry.Name(), entry.IsDir())
}
// Walk directory tree
filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
fmt.Println(path)
return nil
})
// File info
info, _ := os.Stat("file.txt")
fmt.Println(info.Size()) // bytes
fmt.Println(info.ModTime()) // last modified
fmt.Println(info.IsDir())
// Check existence
if _, err := os.Stat("file.txt"); os.IsNotExist(err) {
fmt.Println("file does not exist")
}
// Remove
os.Remove("file.txt")
os.RemoveAll("directory") // recursiveEntorno y Línea de Comandos
os.Getenv/LookupEnv/Setenv gestionan variables de entorno. os.Args da argumentos de línea de comandos raw. El paquete flag proporciona flags parseados con defaults y texto de ayuda. LookupEnv distingue entre valores no establecidos y vacíos. Las variables de entorno son la forma estándar de configurar apps 12-factor (claves API, URLs de bases de datos).
// Environment variables
os.Setenv("API_KEY", "secret")
key := os.Getenv("API_KEY") // "" if not set
// With existence check
if val, ok := os.LookupEnv("HOME"); ok {
fmt.Println("HOME:", val)
}
// All environment variables
for _, e := range os.Environ() {
fmt.Println(e)
}
// Command line arguments
// args := os.Args // []string, args[0] is program name
// Using flag package
var port int
var host string
flag.IntVar(&port, "port", 8080, "server port")
flag.StringVar(&host, "host", "localhost", "server host")
flag.Parse()
fmt.Printf("Running on %s:%d\n", host, port)
// Run: go run main.go -port 3000 -host 0.0.0.0Ejecutar Comandos
os/exec ejecuta comandos externos. Command() crea el comando; Output() captura stdout; Run() ejecuta con Stdout/Stderr personalizados. Usa CommandContext para timeouts (mata el proceso). Comprueba siempre los errores: exec.ExitError indica códigos de salida no cero. Ten cuidado con la entrada del usuario para prevenir inyección de comandos.
import "os/exec"
// Run a command and get output
cmd := exec.Command("ls", "-la", "/tmp")
output, err := cmd.Output()
if err != nil {
log.Fatal(err)
}
fmt.Println(string(output))
// Capture stdout and stderr separately
cmd = exec.Command("git", "status")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err = cmd.Run()
fmt.Println("stdout:", stdout.String())
fmt.Println("stderr:", stderr.String())
// Pipe input to command
cmd = exec.Command("grep", "error")
cmd.Stdin = strings.NewReader("info\nerror\nwarn\n")
result, _ := cmd.Output()
fmt.Println(string(result)) // "error"
// Run with context (timeout)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cmd = exec.CommandContext(ctx, "sleep", "10")
err = cmd.Run() // killed after 5sJSON y Encoding
JSON Marshal y Unmarshal
encoding/json convierte entre structs de Go y JSON. Marshal (struct → JSON), Unmarshal (JSON → struct). Los struct tags (json:"name") controlan el nombrado y visibilidad de campos. omitempty omite valores cero/vacíos. json:"-" excluye un campo completamente. Es la forma estándar de manejar peticiones/respuestas de API en Go.
import "encoding/json"
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"` // omit if empty
Password string `json:"-"` // never serialize
Age int `json:"age,omitempty"`
}
// Struct to JSON (marshal)
user := User{ID: 1, Name: "Alice", Age: 30}
data, err := json.Marshal(user)
// {"id":1,"name":"Alice","age":30}
// Pretty print
pretty, _ := json.MarshalIndent(user, "", " ")
// JSON to struct (unmarshal)
jsonStr := `{"id":2,"name":"Bob","email":"[email protected]"}`
var u User
err := json.Unmarshal([]byte(jsonStr), &u)
fmt.Println(u.Name) // "Bob"
// Struct tags control field names:
// json:"name" -> rename
// json:"name,omitempty" -> skip if zero value
// json:"-" -> skip entirely
// json:",omitempty" -> keep name, skip if emptyJSON con Maps y Slices
Para JSON dinámico (estructura desconocida), unmarshal en map[string]any (o interface{}). Los números JSON se convierten en float64: type-assert para acceder. json.Decoder/Encoder trabajan con streams (archivos, bodies HTTP) eficientemente. Usa unmarshaling de struct cuando conoces el schema; usa maps para datos flexibles/dinámicos.
// Parse arbitrary JSON into map[string]interface{}
jsonStr := `{"name":"Alice","scores":[90,85,92],"active":true}`
var result map[string]any
json.Unmarshal([]byte(jsonStr), &result)
// Type-assert to access values
name := result["name"].(string)
scores := result["scores"].([]any)
for _, s := range scores {
fmt.Println(s.(float64)) // JSON numbers are float64
}
// Encode a map
m := map[string]any{
"count": 42,
"items": []string{"a", "b", "c"},
"meta": map[string]any{"page": 1},
}
data, _ := json.Marshal(m)
// Decode JSON stream
dec := json.NewDecoder(strings.NewReader(jsonStr))
var v map[string]any
dec.Decode(&v)
// Encode to stream
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
enc.Encode(user)Marshaling JSON Personalizado
Implementa MarshalJSON/UnmarshalJSON para serialización personalizada. Es útil para: campos calculados, formatos alternativos (Money como '99.99 USD'), manejo de datos sensibles y formateo de tiempo. El receptor del método para UnmarshalJSON debe ser un puntero para modificar el struct. time.Time se serializa automáticamente como cadenas RFC 3339.
// Implement MarshalJSON/UnmarshalJSON
type Money struct {
Amount float64
Currency string
}
func (m Money) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
"amount": m.Amount,
"currency": m.Currency,
"display": fmt.Sprintf("%.2f %s", m.Amount, m.Currency),
})
}
func (m *Money) UnmarshalJSON(data []byte) error {
var v struct {
Amount float64
Currency string
}
if err := json.Unmarshal(data, &v); err != nil {
return err
}
m.Amount = v.Amount
m.Currency = v.Currency
return nil
}
money := Money{Amount: 99.99, Currency: "USD"}
data, _ := json.Marshal(money)
// {"amount":99.99,"currency":"USD","display":"99.99 USD"}
// Time formatting
type Event struct {
Time time.Time `json:"time"`
}
// time.Time marshals as RFC 3339 by defaultServidor HTTP
El paquete net/http construye servidores HTTP. http.HandleFunc registra handlers. http.ResponseWriter escribe la respuesta; *http.Request lee la petición. Para APIs JSON, establece Content-Type y usa json.NewEncoder(w).Encode(data). La biblioteca estándar está lista para producción: no se necesita framework para APIs simples. Usa mux routers (gorilla/mux, chi) para routing complejo.
import (
"encoding/json"
"net/http"
)
type Response struct {
Status string `json:"status"`
Message string `json:"message"`
}
func main() {
// Simple handler
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
})
// JSON API endpoint
http.HandleFunc("/api/user", func(w http.ResponseWriter, r *http.Request) {
user := User{ID: 1, Name: "Alice"}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
})
// Start server
fmt.Println("Server running on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
// Request methods:
// r.Method -> "GET", "POST", etc.
// r.URL.Query().Get("key") -> query params
// r.Body -> request body (io.ReadCloser)Cliente HTTP
http.Get/Post son atajos convenientes. Para cabeceras, métodos o bodies personalizados, usa http.NewRequest + client.Do(). Haz siempre defer resp.Body.Close() para evitar fugas de conexión. Establece un timeout en el cliente (por defecto no hay timeout: peligroso). Para producción, reutiliza http.Client (hace connection pooling) y usa context para cancelación.
// Simple GET
resp, err := http.Get("https://api.example.com/users")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
fmt.Println(string(body))
// Custom request with headers
req, err := http.NewRequest("GET", "https://api.example.com/data", nil)
req.Header.Set("Authorization", "Bearer token123")
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
defer resp.Body.Close()
// POST with JSON body
user := User{Name: "Alice"}
body, _ := json.Marshal(user)
resp, err := http.Post(
"https://api.example.com/users",
"application/json",
bytes.NewBuffer(body),
)
defer resp.Body.Close()
// With timeout
client := &http.Client{Timeout: 10 * time.Second}
resp, err = client.Get("https://slow-api.example.com")Testing y Benchmarking
Pruebas Unitarias
Los archivos de prueba terminan con _test.go, las funciones de prueba empiezan con Test. Las pruebas table-driven son el patrón idiomático: define casos de prueba en un slice, itera con t.Run para subtests (con nombre, ejecutables individualmente). Usa t.Errorf para fallos (continúa), t.Fatalf para fallos fatales (detiene). Ejecuta con 'go test -v' para salida verbosa.
// math_test.go (file must end with _test.go)
package math
import "testing"
// Test function: func TestXxx(t *testing.T)
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}
// Table-driven tests (idiomatic Go)
func TestAddTable(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive", 2, 3, 5},
{"negative", -1, -1, -2},
{"zero", 0, 0, 0},
{"mixed", -5, 10, 5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}
// Run: go test -v
// Run specific: go test -run TestAddTable/positiveBenchmarks
Las funciones de benchmark empiezan con Benchmark y usan *testing.B. El bucle b.N lo ajusta el runtime para obtener medidas estables. Ejecuta con 'go test -bench=.'. b.ResetTimer() excluye el tiempo de setup. b.ReportAllocs() muestra asignaciones de memoria. Compara implementaciones con 'benchstat' para verificar que las mejoras son significativas.
// Benchmark function: func BenchmarkXxx(b *testing.B)
func BenchmarkFibonacci(b *testing.B) {
for i := 0; i < b.N; i++ {
Fibonacci(20)
}
}
// Benchmark with allocations
func BenchmarkStringConcat(b *testing.B) {
for i := 0; i < b.N; i++ {
s := ""
for j := 0; j < 100; j++ {
s += "a"
}
}
}
// Run: go test -bench=.
// Output:
// BenchmarkFibonacci-8 300 4234567 ns/op
// BenchmarkStringConcat-8 200 6789012 ns/op 100 B/op 1 allocs/op
// Benchmark with setup
func BenchmarkProcess(b *testing.B) {
data := setup() // setup not measured
b.ResetTimer() // start timing here
for i := 0; i < b.N; i++ {
process(data)
}
}
// Memory allocations
func BenchmarkAllocs(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
make([]int, 100)
}
}Helpers de Test y Mocking
t.Helper() mejora los mensajes de error saltándose las funciones helper en los stack traces. Mock implementando interfaces (el enfoque de Go para mocking: no se necesita framework de mocks). t.Cleanup() registra funciones de limpieza (como defer, pero para el ámbito de test). Para mocking complejo, usa testify/assert y mockery o gomock para generar mocks desde interfaces.
// t.Helper() marks helper functions (better error locations)
func assertEqual(t *testing.T, got, want int) {
t.Helper()
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
// Interfaces for mocking
type DataStore interface {
Get(key string) (string, error)
}
type MockStore struct {
data map[string]string
}
func (m *MockStore) Get(key string) (string, error) {
if v, ok := m.data[key]; ok {
return v, nil
}
return "", errors.New("not found")
}
func TestService(t *testing.T) {
store := &MockStore{data: map[string]string{"key": "value"}}
svc := NewService(store)
result := svc.GetValue("key")
assertEqual(t, result, "value")
}
// Subtests with t.Run for setup/teardown
func TestWithCleanup(t *testing.T) {
t.Cleanup(func() {
// runs after test (LIFO order)
os.Remove("tempfile")
})
// test code
}Lo Destacado de la Biblioteca Estándar
La biblioteca estándar de Go es completa. time maneja fechas/horas (nota: el formateo usa el tiempo de referencia 2006-01-02). regexp para coincidencia de patrones. context para cancelación/timeouts entre goroutines. sync.Pool para reutilización de objetos (reduce la presión del GC). Go 1.21+ añade los paquetes slices y maps con utilidades genéricas.
// time package
now := time.Now()
future := now.Add(24 * time.Hour)
formatted := now.Format("2006-01-02 15:04:05")
parsed, _ := time.Parse("2006-01-02", "2024-01-15")
// regexp
re := regexp.MustCompile(`\d{4}-\d{2}-\d{2}`)
matches := re.FindString("date: 2024-01-15")
all := re.FindAllString("2024-01-15 and 2024-02-20", -1)
// context (cancellation, timeouts)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
go doWork(ctx)
// cancel() or timeout stops doWork
// sync.Pool (reuse objects)
var bufPool = sync.Pool{
New: func() any { return new(bytes.Buffer) },
}
buf := bufPool.Get().(*bytes.Buffer)
defer bufPool.Put(buf)
// sort (Go 1.21+ slices package)
slices.Sort(nums)
slices.Contains(nums, 42)
slices.Reverse(nums)Goroutines en Profundidad
Iniciar Goroutines y WaitGroup
Las goroutines son hilos ligeros gestionados por el runtime de Go (no hilos del SO): puedes spawnear cientos de miles. sync.WaitGroup coordina la finalización de goroutines: Add(1) antes de iniciar, Done() al terminar (usa defer), y Wait() para bloquear hasta que el contador llegue a cero. Pasa siempre un puntero al WaitGroup para que todas las goroutines compartan el mismo contador.
package main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // signal completion when function returns
fmt.Printf("Worker %d started\n", id)
// ... do work ...
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // increment counter before starting goroutine
go worker(i, &wg)
}
wg.Wait() // block until all goroutines call Done()
fmt.Println("All workers finished")
}GOMAXPROCS y Scheduling
GOMAXPROCS controla cuántos hilos del SO ejecutan goroutines simultáneamente: por defecto es el número de núcleos de CPU y casi siempre es óptimo. Go usa planificación M:N: muchas goroutines en pocos hilos del SO. El bloqueo de E/S u operaciones de canal hace que el scheduler ejecute otras goroutines en el mismo hilo. runtime.Gosched() cede explícitamente. Rara vez necesitas ajustar GOMAXPROCS.
package main
import (
"fmt"
"runtime"
)
func main() {
// GOMAXPROCS: number of OS threads available to run goroutines
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0)) // defaults to CPU cores
// Set it manually (rarely needed — default is optimal)
runtime.GOMAXPROCS(2)
// Goroutines are multiplexed onto OS threads (M:N scheduling)
// - Blocking syscalls (I/O) don't block other goroutines
// - The scheduler steals work across threads
// - A goroutine blocked on channel/IO yields its thread
// Number of goroutines currently running
go func() { runtime.Gosched() }() // yield to other goroutines
fmt.Println("Goroutines:", runtime.NumGoroutine())
}Fugas de Goroutines y Prevención
Una fuga de goroutine ocurre cuando una goroutine bloquea para siempre (por ejemplo, enviando en un canal sin búfer que nadie lee): nunca se recolecta. Previene fugas: bufferizando canales, usando select con un canal/contexto de cancelación, y proporcionando siempre una salida. Las goroutines filtradas acumulan memoria y CPU. Usa runtime.NumGoroutine() y pprof para detectar fugas en producción.
package main
// BAD: goroutine leaks — this goroutine never exits
func leaky() <-chan int {
ch := make(chan int)
go func() {
ch <- 42 // blocks forever if nobody reads from ch
}()
return ch
}
// GOOD: use context for cancellation
func safe(ctx context.Context) <-chan int {
ch := make(chan int, 1) // buffered — sender never blocks
go func() {
select {
case ch <- 42:
case <-ctx.Done(): // exit when context is cancelled
return
}
}()
return ch
}
// GOOD: always ensure senders can exit
// 1. Buffer the channel so send doesn't block
// 2. Use select with a done/cancel channel
// 3. Use context.WithCancel/WithTimeout for lifecycle controlsync.Once (Inicialización Única)
sync.Once garantiza que una función se ejecute exactamente una vez en todas las goroutines: la forma estándar de implementar singletons thread-safe e inicialización perezosa. Es más eficiente que la comprobación de flag protegida por mutex. El método Do bloquea a los llamadores concurrentes hasta que la primera invocación completa. sync.Once también se usa internamente en muchos patrones de la biblioteca estándar como sync.OnceValue (Go 1.21+).
package main
import (
"fmt"
"sync"
)
var (
instance *Database
once sync.Once
)
type Database struct{ name string }
func GetDB() *Database {
// sync.Once guarantees the function runs exactly once,
// even if called from many goroutines simultaneously
once.Do(func() {
instance = &Database{name: "production"}
fmt.Println("Database initialized")
})
return instance
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
GetDB() // "Database initialized" prints only once
}()
}
wg.Wait()
}Worker Pool con Goroutines
El patrón worker pool limita la concurrencia a un número fijo de goroutines procesando jobs de un canal. Esto previene el agotamiento de recursos de spawnear goroutines ilimitadas. Los workers usan 'for job := range jobs' que sale cuando se cierra el canal. close(jobs) señaliza a todos los workers que paren. Es el patrón canónico de Go para paralelismo acotado.
package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs { // exits when jobs channel is closed
results <- job * job // process and send result
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
var wg sync.WaitGroup
// Start 3 workers
for w := 1; w <= 3; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
// Send 5 jobs
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs) // signal workers to stop (range loop exits)
wg.Wait()
close(results)
for r := range results {
fmt.Println("Result:", r)
}
}Canales y Select
Fundamentos de Canal (Sin Búfer y Con Búfer)
Los canales sin búfer (make(chan T)) sincronizan emisor y receptor: el envío bloquea hasta que hay un receptor listo. Los canales con búfer (make(chan T, n)) permiten n envíos sin receptor, desacoplando el timing emisor/receptor. Solo el emisor debería cerrar un canal (para señalizar 'no más valores'). Recibir de un canal cerrado devuelve el valor cero con ok=false. Los bucles range salen cuando se cierra el canal.
package main
import "fmt"
func main() {
// Unbuffered: send blocks until someone receives (synchronous)
ch := make(chan int)
go func() {
ch <- 42 // blocks until main reads
}()
fmt.Println(<-ch) // 42
// Buffered: send blocks only when buffer is full
buf := make(chan int, 3)
buf <- 1 // doesn't block (buffer has space)
buf <- 2
buf <- 3
// buf <- 4 // would block — buffer full
fmt.Println(<-buf) // 1 (FIFO)
// Close a channel (sender closes, never the receiver)
close(buf)
// Check if closed (ok is false when closed and empty)
val, ok := <-buf
fmt.Println(val, ok) // 2 true
// Range over channel until closed
nums := make(chan int, 3)
nums <- 10; nums <- 20; nums <- 30
close(nums)
for n := range nums {
fmt.Println(n) // 10, 20, 30
}
}Sentencia Select
select permite a una goroutine esperar múltiples operaciones de canal simultáneamente: elige el primer case listo (aleatoriamente si múltiples están listos). El case por defecto hace select no bloqueante. time.After crea un canal de timeout. select es el corazón de la coordinación concurrente de Go: multiplexación, timeouts, cancelación y patrones fan-in/fan-out se construyen sobre él.
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "from ch1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "from ch2"
}()
// select waits on multiple channel operations
// picks the first one that's ready (random if multiple ready)
for i := 0; i < 2; i++ {
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
}
}
// Non-blocking receive with default
select {
case msg := <-ch1:
fmt.Println(msg)
default:
fmt.Println("no message ready") // runs immediately
}
// Timeout with time.After
select {
case msg := <-ch1:
fmt.Println(msg)
case <-time.After(500 * time.Millisecond):
fmt.Println("timed out")
}
}Patrones Fan-In y Fan-Out
Fan-out distribuye trabajo entre múltiples goroutines para paralelismo; fan-in fusiona múltiples canales en uno. Juntos forman un pipeline paralelo: los workers fan-out procesan independientemente, luego fan-in recoge resultados. El fan-in usa un WaitGroup para cerrar el canal fusionado solo después de que todos los canales de entrada se agoten. Estos patrones son fundamentales para el procesamiento de datos concurrente de Go.
package main
import (
"fmt"
"sync"
)
// Fan-Out: distribute work across multiple goroutines
func fanOut(input <-chan int, n int) []<-chan int {
outputs := make([]<-chan int, n)
for i := 0; i < n; i++ {
outputs[i] = process(input) // each worker reads from same input
}
return outputs
}
func process(input <-chan int) <-chan int {
output := make(chan int)
go func() {
defer close(output)
for v := range input {
output <- v * v // transform
}
}()
return output
}
// Fan-In: merge multiple channels into one
func fanIn(channels ...<-chan int) <-chan int {
var wg sync.WaitGroup
merged := make(chan int)
output := func(c <-chan int) {
defer wg.Done()
for v := range c {
merged <- v
}
}
wg.Add(len(channels))
for _, c := range channels {
go output(c)
}
go func() {
wg.Wait()
close(merged)
}()
return merged
}Canales Direccionales (Solo Envío/Recepción)
Los tipos de canal direccional (<-chan T para solo recepción, chan<- T para solo envío) imponen el uso del canal en tiempo de compilación. Un canal bidireccional se convierte implícitamente a un tipo direccional al pasarse a una función. Esto documenta la intención y previene bugs: una función productora literalmente no puede recibir de su propio canal de salida. Usa tipos direccionales en firmas de función para hacer los contratos claros.
package main
import "fmt"
// <-chan int: receive-only (can read, cannot send)
// chan<- int: send-only (can send, cannot read)
func producer(out chan<- int) { // can only send
for i := 0; i < 3; i++ {
out <- i
}
close(out)
}
func consumer(in <-chan int) { // can only receive
for v := range in {
fmt.Println("consumed:", v)
}
}
func main() {
ch := make(chan int)
// Directional restrictions are enforced at compile time
go producer(ch) // bidirectional chan converts to send-only
consumer(ch) // bidirectional chan converts to receive-only
// This prevents bugs: producer can't accidentally read,
// consumer can't accidentally send or close
}Canales Ticker y Timer
time.Ticker se dispara repetidamente a intervalos: usa ticker.C como canal en select para tareas periódicas. time.Timer se dispara una vez tras una duración. Llama siempre a Stop() en tickers y timers para liberar recursos y evitar fugas. time.After es un timer conveniente de un solo uso que devuelve un canal (pero no puede cancelarse, así que prefiere NewTimer en bucles select para evitar acumulación). Reset te permite reprogramar un timer.
package main
import (
"fmt"
"time"
)
func main() {
// Ticker: fires repeatedly at intervals
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop() // always stop to release resources
go func() {
for t := range ticker.C { // ticker.C is a channel
fmt.Println("Tick at", t)
}
}()
// Timer: fires once after a duration
timer := time.NewTimer(2 * time.Second)
<-timer.C // blocks until timer fires
fmt.Println("Timer fired!")
// time.After: one-shot timer as a channel (no Stop needed)
select {
case <-time.After(1 * time.Second):
fmt.Println("1 second elapsed")
}
// Reset a timer (cancel and reschedule)
timer2 := time.NewTimer(5 * time.Second)
timer2.Reset(100 * time.Millisecond)
<-timer2.C
}Paquete Context
context.WithCancel y WithTimeout
context.Context transporta cancelación, timeouts y valores con ámbito de petición a través de fronteras de goroutine. WithCancel devuelve un contexto y una función cancel; WithTimeout auto-cancela tras una duración. Las goroutines comprueban ctx.Done() (un canal) en un select para saber cuándo parar. LLAMA SIEMPRE a la función cancel (usa defer) para liberar recursos, incluso si el timeout se dispara: de lo contrario el contexto se fuga.
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done(): // cancelled or timed out
fmt.Printf("Worker %d stopped: %v\n", id, ctx.Err())
return
default:
fmt.Printf("Worker %d working...\n", id)
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
// WithCancel: manual cancellation
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx, 1)
time.Sleep(2 * time.Second)
cancel() // stop the worker
time.Sleep(500 * time.Millisecond)
// WithTimeout: auto-cancel after duration
ctx2, cancel2 := context.WithTimeout(context.Background(), 1500*time.Millisecond)
defer cancel2() // always call cancel to release resources
go worker(ctx2, 2)
time.Sleep(2 * time.Second) // worker stops after 1.5s
}Propagar Context a Través de Llamadas
Context debería ser el primer parámetro de toda función que hace E/S, y debería propagarse a través de toda la cadena de llamadas. http.Request.Context() se cancela automáticamente cuando el cliente se desconecta. Pasar ctx a operaciones de base de datos/HTTP (QueryRowContext, NewRequestWithContext) garantiza que se aborten cuando se cancela el contexto: previene trabajo desperdiciado y fugas de recursos. Nunca almacenes contextos en structs.
package main
import (
"context"
"database/sql"
"net/http"
)
// Context should be the FIRST parameter, named ctx
func GetUser(ctx context.Context, db *sql.DB, id int) (string, error) {
// Pass ctx to all blocking operations so they cancel together
var name string
err := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id=?", id).Scan(&name)
return name, err
}
func handler(w http.ResponseWriter, r *http.Request) {
// r.Context() is cancelled when the client disconnects
ctx := r.Context()
name, err := GetUser(ctx, db, 42)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
w.Write([]byte(name))
}
// BEST PRACTICE: pass context through every function in the call chain
// that might do I/O. This ensures a client disconnect or timeout
// cancels ALL in-flight work (DB queries, HTTP calls, etc.)Valores de Context (Datos con Ámbito de Petición)
context.WithValue almacena datos con ámbito de petición (como IDs de usuario, trace IDs, tokens de auth) que fluyen por la cadena de llamadas. Usa un tipo de clave personalizado (no string) para evitar colisiones de claves. Los valores deberían ser datos que la petición necesita, no parámetros de función: el equipo de Go recomienda usarlo con moderación, principalmente para concerns transversales como tracing/auth. Haz siempre type-assert al recuperar valores.
package main
import (
"context"
"fmt"
"net/http"
)
// Define a custom key type to avoid collisions
type contextKey string
const userIDKey contextKey = "userID"
// Set a value in context
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := 42 // authenticate...
// WithValue creates a new context with the value
ctx := context.WithValue(r.Context(), userIDKey, userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// Get a value from context (must type-assert)
func getUserID(ctx context.Context) (int, bool) {
if v, ok := ctx.Value(userIDKey).(int); ok {
return v, true
}
return 0, false
}
func handler(w http.ResponseWriter, r *http.Request) {
if uid, ok := getUserID(r.Context()); ok {
fmt.Fprintf(w, "User ID: %d", uid)
}
}Apagado Elegante con Context
El apagado elegante permite que las peticiones en vuelo completen antes de que el servidor salga. signal.Notify captura señales del SO (Ctrl+C, SIGTERM de orquestadores de contenedores). server.Shutdown(ctx) deja de aceptar nuevas conexiones y espera a que las activas terminen (hasta el timeout del contexto). Es esencial para servidores de producción: sin él, las peticiones activas se terminan abruptamente, causando errores y corrupción de datos.
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
server := &http.Server{Addr: ":8080", Handler: mux}
// Listen for OS interrupt signals (Ctrl+C, SIGTERM)
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
go func() {
log.Println("Server starting on :8080")
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server error: %v", err)
}
}()
<-stop // block until signal received
log.Println("Shutting down...")
// Give in-flight requests 10 seconds to finish
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Printf("Forced shutdown: %v", err)
}
log.Println("Server stopped gracefully")
}Deadline de Context y Manejo de Errores
context.WithDeadline cancela en un tiempo absoluto; WithTimeout cancela tras una duración relativa (WithTimeout es solo WithDeadline(now+timeout)). ctx.Err() devuelve context.DeadlineExceeded o context.Canceled para que puedas distinguir por qué se detuvo. Usa errors.Is() para comprobar errores de context. Comprueba siempre ctx.Err() al inicio de operaciones largas y usa select con ctx.Done() durante esperas bloqueantes.
package main
import (
"context"
"errors"
"fmt"
"time"
)
func slowOperation(ctx context.Context) (string, error) {
// Check if already cancelled before starting
if err := ctx.Err(); err != nil {
return "", err
}
select {
case <-time.After(3 * time.Second): // simulate slow work
return "result", nil
case <-ctx.Done(): // cancelled or deadline exceeded
return "", ctx.Err()
}
}
func main() {
// Deadline: cancel at a specific time
deadline := time.Now().Add(1 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
result, err := slowOperation(ctx)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("Operation timed out")
} else if errors.Is(err, context.Canceled) {
fmt.Println("Operation was cancelled")
}
return
}
fmt.Println(result)
}Servidor y Cliente HTTP
Servidor HTTP (net/http)
net/http proporciona un servidor HTTP listo para producción. http.HandleFunc registra handlers por ruta. Usa json.NewEncoder(w).Encode() para escribir respuestas JSON y json.NewDecoder(r.Body).Decode() para analizar bodies de petición. Establece siempre cabeceras Content-Type y comprueba r.Method. http.Error envía un estado de error. ListenAndServe inicia el servidor: envuélvelo con log.Fatal para capturar errores.
package main
import (
"encoding/json"
"log"
"net/http"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func getUser(w http.ResponseWriter, r *http.Request) {
user := User{ID: 1, Name: "Alice"}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
func createUser(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
user.ID = 99
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
func main() {
http.HandleFunc("/users/1", getUser)
http.HandleFunc("/users", createUser)
log.Println("Server on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}Cliente HTTP y Peticiones
El http.Client por defecto NO tiene timeout: establece siempre uno para evitar colgarse para siempre en servidores que no responden. Usa http.NewRequestWithContext para adjuntar un contexto para cancelación/timeouts. Haz SIEMPRE defer resp.Body.Close() para evitar fugas de conexión. Para producción, reutiliza un único http.Client (gestiona connection pooling). http.Get es un atajo pero carece de timeout y personalización.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
func main() {
// Simple GET
resp, err := http.Get("https://api.example.com/users")
if err != nil {
panic(err)
}
defer resp.Body.Close() // ALWAYS close the body
var users []map[string]any
json.NewDecoder(resp.Body).Decode(&users)
fmt.Println(users)
// Custom client with timeout (default has NO timeout!)
client := &http.Client{Timeout: 30 * time.Second}
// POST with JSON body and context
body, _ := json.Marshal(map[string]string{"name": "Bob"})
req, _ := http.NewRequestWithContext(context.Background(),
"POST", "https://api.example.com/users", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err = client.Do(req)
defer resp.Body.Close()
fmt.Println("Status:", resp.Status)
}Patrón Middleware
El middleware envuelve handlers para añadir concerns transversales (logging, auth, CORS, rate limiting) sin modificar el handler en sí. La firma func(http.Handler) http.Handler es el tipo de middleware estándar. Chain los aplica en orden (el más externo primero). Este patrón es la base de frameworks como Chi, Echo y Gin. El ResponseWriter puede envolverse para capturar códigos de estado para logging.
package main
import (
"log"
"net/http"
"time"
)
// Middleware wraps an http.Handler to add cross-cutting concerns
type Middleware func(http.Handler) http.Handler
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r) // call the wrapped handler
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
func Auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// Chain multiple middlewares
func Chain(h http.Handler, mws ...Middleware) http.Handler {
for i := len(mws) - 1; i >= 0; i-- {
h = mws[i](h)
}
return h
}
func main() {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello!"))
})
// Apply middlewares: Auth -> Logging -> handler
http.Handle("/", Chain(handler, Logging, Auth))
http.ListenAndServe(":8080", nil)
}Servir Archivos Estáticos y Plantillas
http.FileServer sirve archivos estáticos de un directorio; http.StripPrefix elimina el prefijo de ruta para que las rutas de archivo se resuelvan correctamente. html/template renderiza HTML de forma segura (auto-escapa para prevenir XSS). template.Must entra en pánico en errores de parse (bien para startup). Las plantillas usan {{.Field}} para datos y {{range}} para iteración. Para producción, considera embeber archivos con go:embed en lugar de leer de disco.
package main
import (
"html/template"
"net/http"
)
func main() {
// Serve static files (CSS, JS, images)
fs := http.FileServer(http.Dir("./static"))
http.Handle("/static/", http.StripPrefix("/static/", fs))
// HTML templates
tmpl := template.Must(template.ParseFiles("templates/index.html"))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
data := struct {
Title string
Items []string
}{
Title: "My Page",
Items: []string{"Apple", "Banana", "Cherry"},
}
tmpl.Execute(w, data)
})
http.ListenAndServe(":8080", nil)
}
// templates/index.html:
// <h1>{{.Title}}</h1>
// <ul>{{range .Items}}<li>{{.}}</li>{{end}}</ul>Apagado Elegante y go:embed
go:embed empaqueta archivos en el binario compilado en tiempo de build: habilitando despliegues verdaderos de binario único sin dependencias de archivos externos. Usa directivas //go:embed encima de una declaración var. embed.FS es un sistema de archivos virtual de solo lectura. fs.Sub crea un sub-sistema de archivos (elimina el prefijo de directorio). Funciona para assets estáticos, plantillas HTML, migraciones SQL, archivos de configuración y más.
package main
import (
"embed"
"io/fs"
"net/http"
)
//go:embed static/*
var staticFiles embed.FS
func main() {
// Embed static files into the binary (no external files needed)
// Access the embedded filesystem
sub, _ := fs.Sub(staticFiles, "static")
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(sub))))
// go:embed benefits:
// - Single binary deployment (no external files)
// - Files are compiled into the executable
// - Works with templates, configs, migrations, etc.
// Embed a single file
//go:embed config.json
// var configData []byte
// Embed with pattern
//go:embed templates/*.html
// var templates embed.FS
http.ListenAndServe(":8080", nil)
}Paquete Sync
sync.Mutex y sync.RWMutex
sync.Mutex proporciona locking exclusivo: solo una goroutine puede mantenerlo a la vez. sync.RWMutex permite múltiples lectores concurrentes O un escritor exclusivo: úsalo cuando las lecturas superan ampliamente a las escrituras. Empareja siempre Lock con defer Unlock para prevenir deadlocks si la función entra en pánico. Embebe el mutex en el struct (minúscula mu) para mantenerlo privado. Nunca copies un mutex (usa siempre punteros).
package main
import (
"fmt"
"sync"
)
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock() // always unlock with defer
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
// RWMutex: allows multiple readers OR one writer
type SafeCache struct {
mu sync.RWMutex
data map[string]string
}
func (c *SafeCache) Get(key string) (string, bool) {
c.mu.RLock() // read lock (multiple readers OK)
defer c.mu.RUnlock()
val, ok := c.data[key]
return val, ok
}
func (c *SafeCache) Set(key, val string) {
c.mu.Lock() // write lock (exclusive)
defer c.mu.Unlock()
c.data[key] = val
}sync.Map (Map Concurrente)
sync.Map es un map concurrente-safe optimizado para escenarios específicos: write-once-read-many (cachés) o acceso a claves disjuntas entre goroutines. Evita locks en lecturas vía operaciones atómicas. Sin embargo, para maps concurrentes de propósito general, un map regular protegido por sync.RWMutex suele ser más rápido y ergonómico. La API de sync.Map usa any (interface{}) para claves y valores, perdiendo type safety.
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// Store and Load (thread-safe, no locks needed)
m.Store("name", "Alice")
m.Store("age", 30)
val, ok := m.Load("name")
fmt.Println(val, ok) // Alice true
// LoadOrStore: atomic get-or-set
actual, loaded := m.LoadOrStore("name", "Bob")
fmt.Println(actual, loaded) // Alice true (already existed)
// Delete
m.Delete("age")
// Range over all entries
m.Range(func(key, value any) bool {
fmt.Printf("%v = %v\n", key, value)
return true // continue; return false to stop
})
// Use sync.Map when:
// 1. Keys are written once, read many times (caches)
// 2. Multiple goroutines read/write disjoint keys
// For most cases, a regular map + Mutex is simpler and faster
}sync.Cond (Variables de Condición)
sync.Cond implementa variables de condición: las goroutines esperan a que una condición se vuelva verdadera. Wait() libera atómicamente el lock y duerme; Signal() despierta a un waiter, Broadcast() despierta a todos. Usa siempre un bucle for alrededor de Wait() (no if) para manejar wakeups espurios. Cond es útil para colas productor-consumidor y esperar cambios de estado, aunque los canales suelen proporcionar una alternativa más simple.
package main
import (
"fmt"
"sync"
"time"
)
type Queue struct {
items []int
mu sync.Mutex
cond *sync.Cond
}
func NewQueue() *Queue {
q := &Queue{}
q.cond = sync.NewCond(&q.mu) // cond is tied to the mutex
return q
}
func (q *Queue) Put(item int) {
q.mu.Lock()
defer q.mu.Unlock()
q.items = append(q.items, item)
q.cond.Signal() // wake one waiting goroutine
}
func (q *Queue) Get() int {
q.mu.Lock()
defer q.mu.Unlock()
for len(q.items) == 0 {
q.cond.Wait() // atomically unlocks mu, sleeps, re-locks on wake
}
item := q.items[0]
q.items = q.items[1:]
return item
}
func main() {
q := NewQueue()
go func() {
time.Sleep(1 * time.Second)
q.Put(42)
}()
fmt.Println("Waiting for item...")
fmt.Println("Got:", q.Get()) // blocks until item available
}sync.Pool (Reutilización de Objetos)
sync.Pool reutiliza objetos para reducir asignaciones y presión del GC: ideal para objetos de corta vida frecuentemente asignados como bytes.Buffer. Get() devuelve un objeto del pool (llamando a New si está vacío); Put() lo devuelve. Los pools se limpian durante el GC, así que no confíes en ellos para persistencia. Resetea siempre los objetos antes de reutilizar. La biblioteca estándar usa sync.Pool extensivamente (http, json, fmt).
package main
import (
"bytes"
"sync"
)
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer) // create when pool is empty
},
}
func Process(data []byte) string {
// Get a buffer from the pool (or create via New)
buf := bufPool.Get().(*bytes.Buffer)
defer bufPool.Put(buf) // return it when done
buf.Reset() // clear before reuse
buf.Write(data)
buf.WriteString("-processed")
return buf.String()
}
// Benefits:
// - Reduces GC pressure by reusing objects
// - Avoids allocation overhead for short-lived objects
// - Pool entries can be garbage collected between GC cycles
//
// Use for: bytes.Buffer, json encoders, temporary slices
// NOT for: long-lived objects or when size is unpredictableerrgroup (Grupo con Propagación de Errores)
errgroup (de golang.org/x/sync) es un WaitGroup que devuelve el primer error y puede cancelar las goroutines restantes vía context. g.Go() inicia una goroutine; g.Wait() bloquea y devuelve el primer error no nil. WithContext crea un contexto que se cancela cuando cualquier goroutine devuelve un error: así otras goroutines paran temprano. Es la forma idiomática de ejecutar operaciones paralelas que deberían tener éxito o fallar todas juntas.
package main
import (
"context"
"fmt"
"golang.org/x/sync/errgroup"
"net/http"
)
// errgroup is like WaitGroup but propagates the first error
// and can cancel remaining goroutines on failure
func fetchAll(urls []string) ([]*http.Response, error) {
g, ctx := errgroup.WithContext(context.Background())
results := make([]*http.Response, len(urls))
for i, url := range urls {
i, url := i, url // capture loop variables
g.Go(func() error {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err // cancels ctx, stopping other goroutines
}
results[i] = resp
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err // first error returned
}
return results, nil
}
func main() {
urls := []string{
"https://example.com",
"https://golang.org",
}
resps, err := fetchAll(urls)
fmt.Println(len(resps), err)
}Genéricos (Go 1.18+)
Funciones Genéricas y Parámetros de Tipo
Los genéricos de Go (1.18+) usan parámetros de tipo entre corchetes: [T any] declara T como cualquier tipo. El compilador infiere los tipos desde los argumentos, así que rara vez los especificas explícitamente. Los genéricos habilitan funciones reutilizables type-safe como Map/Filter/Reduce sin interface{} y type assertions. 'any' es un alias de interface{} introducido con los genéricos.
package main
import "fmt"
// Type parameters in brackets: [T any] means T can be any type
func Map[T, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}
func Filter[T any](slice []T, predicate func(T) bool) []T {
var result []T
for _, v := range slice {
if predicate(v) {
result = append(result, v)
}
}
return result
}
func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
result := initial
for _, v := range slice {
result = fn(result, v)
}
return result
}
func main() {
nums := []int{1, 2, 3, 4, 5}
squared := Map(nums, func(n int) int { return n * n })
fmt.Println(squared) // [1 4 9 16 25]
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
fmt.Println(evens) // [2 4]
sum := Reduce(nums, 0, func(acc, n int) int { return acc + n })
fmt.Println(sum) // 15
// Works with any type
words := []string{"go", "rust", "python"}
lengths := Map(words, func(s string) int { return len(s) })
fmt.Println(lengths) // [2 4 6]
}Restricciones de Tipo y comparable
Las restricciones de tipo limitan qué tipos acepta un genérico. 'comparable' es una restricción integrada para tipos que soportan == (necesario para claves de map y comparaciones). Las restricciones personalizadas usan uniones de tipos (int | float64) para permitir tipos específicos. El paquete golang.org/x/exp/constraints proporciona Ordered (para <, >, etc.). Las restricciones pueden combinar conjuntos de tipos y métodos.
package main
import "fmt"
// comparable: types that support == and != (built-in constraint)
func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}
// Custom constraint with a type set
type Number interface {
int | int64 | float64 | float32
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
// Multiple type parameters
func Pair[T, U any](first T, second U) struct{ First T; Second U } {
return struct{ First T; Second U }{first, second}
}
func main() {
fmt.Println(Contains([]string{"a", "b", "c"}, "b")) // true
fmt.Println(Contains([]int{1, 2, 3}, 5)) // false
fmt.Println(Sum([]int{1, 2, 3})) // 6
fmt.Println(Sum([]float64{1.5, 2.5})) // 4
p := Pair("Alice", 30)
fmt.Println(p) // {Alice 30}
}Estructuras de Datos Genéricas
Los tipos genéricos (Stack[T any]) crean estructuras de datos type-safe que funcionan con cualquier tipo sin boxing de interface{} o type assertions. El parámetro de tipo T es parte del tipo, así que Stack[int] y Stack[string] son tipos distintos comprobados en tiempo de compilación. Esto elimina toda una clase de errores de tipo en tiempo de ejecución. Los structs, métodos e interfaces genéricos admiten parámetros de tipo.
package main
import "fmt"
// Generic Stack — works with any type, type-safe
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
index := len(s.items) - 1
item := s.items[index]
s.items = s.items[:index]
return item, true
}
func (s *Stack[T]) Len() int {
return len(s.items)
}
func main() {
// Type-safe stack of ints
intStack := &Stack[int]{}
intStack.Push(1)
intStack.Push(2)
val, _ := intStack.Pop()
fmt.Println(val) // 2
// Type-safe stack of strings
strStack := &Stack[string]{}
strStack.Push("hello")
s, _ := strStack.Pop()
fmt.Println(s) // hello
// intStack.Push("oops") // compile error! type-safe
}Restricciones Genéricas con ~ (Tipo Subyacente)
El prefijo ~ en una restricción coincide con cualquier tipo cuyo tipo subyacente sea el tipo nombrado: así ~string coincide tanto con string como con type MyString string. Sin ~, una restricción solo coincide con el tipo nombrado exacto, lo cual rara vez es útil para tipos personalizados. Usa ~ cuando quieras que los genéricos funcionen con alias de tipos y tipos nombrados derivados de primitivos (común en modelado de dominio).
package main
import "fmt"
// ~ allows any type whose UNDERLYING type matches
// (e.g., type MyString string would match ~string)
type StringLike interface {
~string
}
func Join[S StringLike](items []S, sep string) string {
result := ""
for i, item := range items {
if i > 0 {
result += sep
}
result += string(item) // convert underlying type to string
}
return result
}
// Numeric constraint with ~
type Integer interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
func Double[T Integer](v T) T {
return v * 2
}
// Custom type based on int
type Score int
func main() {
type MyString string
items := []MyString{"a", "b", "c"}
fmt.Println(Join(items, "-")) // a-b-c
s := Double(Score(21))
fmt.Println(s) // 42
}Genéricos vs Interfaces (Cuándo Usar)
Elige genéricos cuando el algoritmo es idéntico entre tipos (colecciones, matemáticas, transformaciones): proporcionan type safety en tiempo de compilación con sobrecarga cero en tiempo de ejecución. Elige interfaces cuando diferentes tipos necesitan diferentes implementaciones (polimorfismo) o cuando necesitas dispatch en tiempo de ejecución (inyección de dependencias, mocking). Son complementarios: los genéricos pueden restringirse por interfaces (T Stringer) para lo mejor de ambos mundos.
package main
import "fmt"
// GENERICS: best for algorithms/data structures that work
// the same way regardless of type (collections, sorting, mapping)
func Max[T int | float64](a, b T) T {
if a > b {
return a
}
return b
}
// INTERFACES: best when different types need DIFFERENT implementations
// (polymorphism, dependency injection, mocking)
type Stringer interface {
String() string
}
type Dog struct{ Name string }
func (d Dog) String() string { return "Dog: " + d.Name }
type Cat struct{ Name string }
func (c Cat) String() string { return "Cat: " + c.Name }
func Print[T Stringer](v T) {
fmt.Println(v.String())
}
func main() {
fmt.Println(Max(3, 7)) // 7
fmt.Println(Max(3.14, 2.71)) // 3.14
Print(Dog{"Rex"}) // Dog: Rex
Print(Cat{"Whiskers"}) // Cat: Whiskers
}
// RULE OF THUMB:
// - Same logic, different types -> Generics
// - Different logic, shared contract -> Interfaces
// - Need runtime polymorphism -> Interfaces
// - Need compile-time type safety -> GenericsReflection
reflect.Type y reflect.Value
reflect.TypeOf devuelve el tipo en tiempo de ejecución; reflect.ValueOf devuelve el valor en tiempo de ejecución. Desde un Type puedes inspeccionar campos de struct, métodos y tags. Desde un Value puedes leer y (con CanSet) modificar campos. Reflection es potente pero lento (10-100x más lento que acceso directo) y evita el type safety en tiempo de compilación: úsalo con moderación, principalmente para serialización, ORMs y frameworks.
package main
import (
"fmt"
"reflect"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
u := User{Name: "Alice", Age: 30}
// Get type and value at runtime
t := reflect.TypeOf(u)
v := reflect.ValueOf(u)
fmt.Println("Type:", t.Name()) // User
fmt.Println("Kind:", t.Kind()) // struct
fmt.Println("NumFields:", t.NumField())
// Iterate struct fields
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
tag := field.Tag.Get("json")
fmt.Printf("%s (%s) = %v [json:%s]\n",
field.Name, field.Type, value, tag)
}
// Name (string) = Alice [json:name]
// Age (int) = 30 [json:age]
}Modificar Valores con Reflection
Para modificar un valor vía reflection, debes pasar un puntero y llamar .Elem() para desreferenciarlo. CanSet() informa si un campo es asignable (campos exportados de un valor addressable). Los campos no exportados (minúsculas) pueden leerse pero no establecerse. SetInt/SetString/Set tienen comprobación de tipo en tiempo de ejecución y entran en pánico si no coinciden. La mutación basada en reflection es la base de los parsers de config y ORMs.
package main
import (
"fmt"
"reflect"
)
func main() {
type Config struct {
Port int
Host string
}
cfg := Config{Port: 8080, Host: "localhost"}
// MUST pass a pointer to modify (ValueOf of a struct is a copy)
v := reflect.ValueOf(&cfg).Elem()
// Check if a field is settable
portField := v.FieldByName("Port")
fmt.Println("Settable:", portField.CanSet()) // true
// Modify fields by name
portField.SetInt(9090)
v.FieldByName("Host").SetString("0.0.0.0")
fmt.Println(cfg) // {9090 0.0.0.0}
// Set via interface (type-checked at runtime)
v.FieldByName("Port").Set(reflect.ValueOf(7070))
fmt.Println(cfg) // {7070 0.0.0.0}
// Unexported fields are NOT settable (CanSet returns false)
}Llamar Métodos Dinámicamente
Reflection puede llamar métodos dinámicamente por nombre: útil para frameworks RPC, sistemas de plugins y routing. MethodByName devuelve un Value; Call() lo invoca con un slice de argumentos reflect.Value y devuelve un slice de resultados. NumMethod/Method enumeran métodos. Las llamadas a métodos vía reflection son lentas y evitan el type safety, así que úsalas solo cuando el nombre del método no se conoce en tiempo de compilación.
package main
import (
"fmt"
"reflect"
)
type Calculator struct{}
func (c Calculator) Add(a, b int) int { return a + b }
func (c Calculator) Mul(a, b int) int { return a * b }
func main() {
calc := Calculator{}
v := reflect.ValueOf(calc)
// Find a method by name
method := v.MethodByName("Add")
fmt.Println("Found:", method.IsValid()) // true
// Call with arguments (each arg must be a reflect.Value)
args := []reflect.Value{
reflect.ValueOf(3),
reflect.ValueOf(4),
}
results := method.Call(args)
fmt.Println("3 + 4 =", results[0].Int()) // 7
// List all methods
t := reflect.TypeOf(calc)
for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i)
fmt.Printf("Method: %s, Type: %v\n", m.Name, m.Type)
}
// Method: Add, Type: func(main.Calculator, int, int) int
// Method: Mul, Type: func(main.Calculator, int, int) int
}Uso Práctico: Struct a Map (Basado en Tags)
Este patrón (iterar campos de struct y leer tags) es como funcionan encoding/json, los parsers YAML, ORMs y librerías de validación bajo el capó. reflect permite escribir código genérico que procesa cualquier struct basándose en sus tags. La convención del tag json:'-' (saltar este campo) es estándar. Es uno de los usos más legítimos de reflection en Go.
package main
import (
"fmt"
"reflect"
)
// Convert any struct to a map using struct tags as keys
func StructToMap(obj any, tag string) map[string]any {
result := make(map[string]any)
v := reflect.ValueOf(obj)
t := reflect.TypeOf(obj)
// Dereference pointers
if v.Kind() == reflect.Ptr {
v = v.Elem()
t = t.Elem()
}
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
// Use the tag value if present, else the field name
key := field.Tag.Get(tag)
if key == "" {
key = field.Name
}
// Skip fields tagged with "-"
if key == "-" {
continue
}
result[key] = v.Field(i).Interface()
}
return result
}
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Pass string `json:"-"` // excluded
}
func main() {
u := User{Name: "Alice", Age: 30, Pass: "secret"}
m := StructToMap(u, "json")
fmt.Println(m) // map[age:30 name:Alice]
}Build y Tooling
go build, go run y go install
go build compila a un ejecutable; go run compila a un archivo temporal y lo ejecuta (genial para desarrollo); go install pone el binario en $GOPATH/bin para acceso global. La característica destacada de Go es la cross-compilación fácil vía GOOS/GOARCH: sin toolchain necesario. -ldflags='-s -w' elimina info de depuración para binarios ~30% más pequeños. -X inyecta valores (como cadenas de versión) en tiempo de build para CI/CD.
# Build an executable (outputs to current directory)
go build -o myapp # custom output name
go build -o bin/myapp ./cmd # build a specific package
# Build for different OS/arch (cross-compilation)
GOOS=linux GOARCH=amd64 go build -o myapp-linux
GOOS=windows GOARCH=amd64 go build -o myapp.exe
GOOS=darwin GOARCH=arm64 go build -o myapp-mac # Apple Silicon
# Run without building an executable (compiles to temp, runs, deletes)
go run main.go # run single file
go run ./cmd/server # run a package
# Install: builds and puts binary in $GOPATH/bin
go install ./cmd/myapp # now 'myapp' is on your PATH
# Build flags
go build -v # verbose: print packages being compiled
go build -race # enable race detector (for testing)
go build -ldflags="-s -w" # strip debug info (smaller binary)
go build -ldflags="-X main.Version=1.0.0" # inject version at build timego mod (Gestión de Módulos)
go mod gestiona dependencias vía go.mod (lista de dependencias) y go.sum (checksums para seguridad). go get añade/actualiza deps; go mod tidy sincroniza los archivos (ejecuta antes de commits). Go usa Semantic Import Versioning: v2+ requiere un sufijo de ruta /v2. La caché de módulos se comparte en $GOPATH/pkg/mod. go mod vendor crea un directorio vendor/ para builds reproducibles, offline o auditados.
# Initialize a new module
go mod init github.com/user/myproject
# This creates go.mod:
# module github.com/user/myproject
# go 1.21
# Add a dependency (auto-added to go.mod)
go get github.com/gin-gonic/gin@latest
go get github.com/lib/[email protected] # specific version
# Tidy: add missing deps, remove unused ones
go mod tidy
# Download dependencies to local cache
go mod download
# Verify dependencies haven't been modified
go mod verify
# Vendor: copy deps into ./vendor (for offline builds)
go mod vendor
# Upgrade dependencies
go get -u github.com/lib/pq # upgrade to latest minor
go get -u=patch github.com/lib/pq # upgrade patches only
go get github.com/lib/[email protected] # pin specific version
# View dependency graph
go mod graph | headgo test y Benchmarking
go test ejecuta archivos _test.go; las funciones llamadas TestXxx(t *testing.T) son tests, BenchmarkXxx(b *testing.B) son benchmarks. -race habilita el detector de data races (esencial para código concurrente). -cover muestra cobertura de tests; -coverprofile genera un informe detallado. Los benchmarks ejecutan b.N iteraciones donde N se auto-ajusta. Las pruebas table-driven (slices de input/expected) son el estilo de testing idiomático de Go.
# Run all tests in the current package
go test ./...
# Verbose output
go test -v ./...
# Run a specific test
go test -run TestAdd -v
# Run benchmarks
go test -bench=. -benchmem
# Race detector (find data races)
go test -race ./...
# Coverage
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out # view in browser
# --- test file example (xxx_test.go) ---
# func TestAdd(t *testing.T) {
# got := Add(2, 3)
# if got != 5 {
# t.Errorf("Add(2,3) = %d, want 5", got)
# }
# }
#
# func BenchmarkAdd(b *testing.B) {
# for i := 0; i < b.N; i++ {
# Add(2, 3)
# }
# }go fmt, go vet y golangci-lint
go fmt/gofmt impone un único formato canónico: Go no tiene debates de formateo. go vet captura bugs comunes (desajustes de printf, copias de lock, struct tags malos). golangci-lint agrega docenas de linters y es el estándar de la industria para CI. Ejecuta gofmt y go vet antes de cada commit; añade golangci-lint a CI para análisis más profundo. El tooling consistente es una razón principal por la que los codebases de Go se ven uniformes.
# go fmt: format code (the ONE true Go style)
go fmt ./... # format all files
gofmt -d main.go # show diff without changing
gofmt -w main.go # write changes in place
# go vet: static analysis for common mistakes
go vet ./... # check all packages
# Catches: printf format mismatches, unreachable code,
# struct tag errors, lock copies, shadowed variables
# golangci-lint: meta-linter (runs many linters)
# Install: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
golangci-lint run # run all enabled linters
golangci-lint run --enable=gosec,goconst # enable specific linters
# Common linters:
# errcheck - check unchecked errors
# gosec - security issues
# govet - go vet checks
# staticcheck - advanced static analysis
# ineffassign - detect ineffectual assignments
# unused - find unused code
# goimports: fmt + auto-manage imports
# go install golang.org/x/tools/cmd/goimports@latest
goimports -w main.goProfiling y pprof
pprof es el profiler integrado de Go: profiling de CPU, memoria, goroutine y mutex. Para servidores de larga duración, importa _ 'net/http/pprof' para exponer un endpoint /debug/pprof/ para profiling en vivo sin reinicios. go tool pprof da un shell interactivo (top, list, web) o una UI web. El execution tracer (go tool trace) visualiza la planificación de goroutines y el bloqueo. El profiling es esencial para código Go crítico en rendimiento.
# CPU profiling
go test -cpuprofile=cpu.prof -bench=.
go tool pprof cpu.prof
# Interactive commands: top, list FunctionName, web (graphviz)
# Memory profiling
go test -memprofile=mem.prof -bench=.
go tool pprof mem.prof
# Built-in HTTP pprof endpoint (for production servers)
import _ "net/http/pprof"
# Then visit: http://localhost:8080/debug/pprof/
# Capture a 30-second CPU profile:
# go tool pprof http://localhost:8080/debug/pprof/profile?seconds=30
# In code:
import "runtime/pprof"
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// ... code to profile ...
# Trace (execution tracer)
go test -trace=trace.out -bench=.
go tool trace trace.out # opens in browserBase de Datos (SQL)
Fundamentos de database/sql
database/sql es la interfaz estándar para bases de datos SQL. El driver se importa por sus efectos secundarios (registrándose a sí mismo). sql.Open no establece conexión: usa Ping para verificar. Haz siempre defer db.Close().
import "database/sql"
import _ "github.com/lib/pq"
db, err := sql.Open("postgres", "host=localhost dbname=mydb")
if err != nil { log.Fatal(err) }
defer db.Close()
err = db.Ping()Consultar Filas
Query devuelve múltiples filas. Haz siempre defer rows.Close() para liberar recursos. Scan copia valores de columnas a variables. Comprueba rows.Err() después del bucle para errores de iteración.
rows, err := db.Query("SELECT id, name FROM users WHERE age > $1", 18)
if err != nil { log.Fatal(err) }
defer rows.Close()
for rows.Next() {
var id int; var name string
if err := rows.Scan(&id, &name); err != nil { log.Fatal(err) }
fmt.Printf("%d: %s\n", id, name)
}Consultar una Sola Fila
QueryRow devuelve una sola fila. Scan devuelve sql.ErrNoRows si no hay fila coincidente. Más limpio que Query para lookups de una sola fila. Maneja siempre ErrNoRows explícitamente.
var name string
err := db.QueryRow("SELECT name FROM users WHERE id = $1", 1).Scan(&name)
if err != nil {
if err == sql.ErrNoRows { fmt.Println("Not found") } else { log.Fatal(err) }
}Sentencias Preparadas
Prepare crea una sentencia reutilizable, mejorando el rendimiento para consultas repetidas. Previene inyección SQL. Haz siempre defer stmt.Close(). Úsalo para consultas ejecutadas múltiples veces.
stmt, err := db.Prepare("INSERT INTO users(name) VALUES($1)")
if err != nil { log.Fatal(err) }
defer stmt.Close()
_, err = stmt.Exec("Alice")
_, err = stmt.Exec("Bob")Transacciones
Begin inicia una transacción. Todas las operaciones dentro de tx son atómicas. defer Rollback es seguro: es un no-op después de Commit. Si cualquier operación falla, Rollback deshace todos los cambios.
tx, err := db.Begin()
if err != nil { log.Fatal(err) }
defer tx.Rollback() // Safe to call after commit
_, err = tx.Exec("UPDATE accounts SET bal = bal - 100 WHERE id = 1")
_, err = tx.Exec("UPDATE accounts SET bal = bal + 100 WHERE id = 2")
if err != nil { log.Fatal(err) }
err = tx.Commit()Testing en Profundidad
Test Básico
Las funciones de prueba empiezan con Test y toman *testing.T. t.Errorf registra el fallo y continúa. t.Fatalf registra y detiene. Ejecuta con go test. Usa testify/assert para aserciones más limpias.
func TestAdd(t *testing.T) {
got := Add(2, 3)
want := 5
if got != want {
t.Errorf("Add(2,3) = %d; want %d", got, want)
}
}Pruebas Table-Driven
Las pruebas table-driven son idiomáticas en Go. Define casos de prueba como un slice de structs. Itera y ejecuta cada uno. t.Run crea subtests para fallos individuales.
func TestAdd(t *testing.T) {
tests := []struct {
a, b, want int
}{
{1, 2, 3}, {-1, 1, 0}, {0, 0, 0},
}
for _, tt := range tests {
got := Add(tt.a, tt.b)
if got != tt.want {
t.Errorf("Add(%d,%d)=%d; want %d", tt.a, tt.b, got, tt.want)
}
}
}Subtests
Los subtests usan t.Run con un nombre y una función. Ejecuta subtests específicos con patrón -run. Proporciona mejor organización y salida de tests.
func TestAdd(t *testing.T) {
t.Run("positive", func(t *testing.T) {
if Add(1, 2) != 3 { t.Error("failed") }
})
t.Run("negative", func(t *testing.T) {
if Add(-1, -2) != -3 { t.Error("failed") }
})
}
// Run specific: go test -run TestAdd/negativeTest Main
TestMain se ejecuta una vez para el paquete, reemplazando el runner de tests por defecto. Úsalo para setup/teardown global. Debe llamar a m.Run() para ejecutar los tests. os.Exit propaga el código de salida.
func TestMain(m *testing.M) {
setup()
code := m.Run()
teardown()
os.Exit(code)
}
func setup() { /* initialize DB, etc. */ }
func teardown() { /* cleanup */ }Mocking de Interfaces
El mocking en Go se basa en interfaces. Define una interfaz, implementa un mock e inyéctalo. Herramientas como mockery y mockgen auto-generan mocks. Habilita testing unitario sin dependencias externas.
type Store interface { Get(id int) (string, error) }
type MockStore struct { data map[int]string }
func (m *MockStore) Get(id int) (string, error) {
if v, ok := m.data[id]; ok { return v, nil }
return "", errors.New("not found")
}
// Use in tests by injecting MockStoreBenchmarking
Benchmark Básico
Las funciones de benchmark empiezan con Benchmark y toman *testing.B. b.N lo ajusta el framework para obtener medidas estables. Ejecuta con go test -bench. La salida muestra ns/op (nanosegundos por operación).
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
// Run: go test -bench=.
// Output: BenchmarkAdd-8 1000000000 0.3 ns/opSub-Benchmarks
Los sub-benchmarks usan b.Run. ResetTimer excluye el tiempo de setup. ReportAllocs muestra asignaciones de memoria. Compara rendimiento entre tamaños de entrada.
func BenchmarkSort(b *testing.B) {
sizes := []int{100, 1000, 10000}
for _, size := range sizes {
b.Run(fmt.Sprintf("size-%d", size), func(b *testing.B) {
data := generateData(size)
b.ResetTimer()
for i := 0; i < b.N; i++ {
Sort(data)
}
})
}
}Asignaciones de Memoria
ReportAllocs muestra asignaciones de memoria por operación. Reducir asignaciones es clave para el rendimiento de Go. Usa sync.Pool, pre-asigna slices y evita concatenación de cadenas innecesaria.
func BenchmarkConcat(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
s := "a" + "b" + "c"
_ = s
}
}
// Output includes allocs/op and B/opBenchmarks Paralelos
RunParallel ejecuta benchmarks concurrentemente entre múltiples goroutines. Útil para probar código thread-safe. pb.Next() itera la distribución de trabajo. Mide throughput bajo concurrencia.
func BenchmarkParallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Add(2, 3)
}
})
}Comparación de Benchmarks
benchstat compara resultados de benchmark estadísticamente. Ejecuta benchmarks múltiples veces con -count=10 para comparación fiable. Ayuda a verificar mejoras de rendimiento y detectar regresiones.
# Run with memory stats
benchstat old.txt new.txt
# Compare two runs
# name old time/op new time/op delta
# Add-8 2.3ns 1.8ns -21.7%Profiling (pprof)
Profiling de CPU
StartCPUProfile escribe el profile de CPU a un archivo. StopCPUProfile vacía los datos. Analiza con go tool pprof. Usa los comandos top, list, web en pprof. Céntrate en las funciones que consumen más CPU.
import "runtime/pprof"
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// Run your code here
// Analyze: go tool pprof cpu.profProfiling de Memoria
WriteHeapProfile captura el estado actual del heap. Llama a runtime.GC() primero para resultados precisos. Establece MemProfileRate = 1 para perfilar cada asignación (más lento pero preciso).
// Add to code
runtime.GC()
f, _ := os.Create("mem.prof")
pprof.WriteHeapProfile(f)
f.Close()
// Or use runtime.MemProfileRate = 1 for all allocationsProfiling HTTP
net/http/pprof registra endpoints de profiling en el mux por defecto. Accede a los profiles vía HTTP sin reiniciar. Útil para diagnósticos de producción. Asegura el endpoint en producción.
import _ "net/http/pprof"
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// Analyze live:
// go tool pprof http://localhost:6060/debug/pprof/profileComandos pprof
top muestra las funciones que consumen más recursos. list muestra el código fuente anotado. web abre un call graph SVG. tree muestra la jerarquía de llamadas. Usa focus para filtrar.
# Start interactive pprof
go tool pprof cpu.prof
(pprof) top 10 # Top functions by CPU
(pprof) list Add # Show source with timings
(pprof) web # Open graph in browser
(pprof) tree # Call tree
(pprof) png > out.png # Save graph as PNGTrace
runtime/trace captura trazas de ejecución: scheduling de goroutines, GC, bloqueo de syscalls. go tool trace abre una UI web. Útil para diagnosticar latencia y problemas de concurrencia.
import "runtime/trace"
f, _ := os.Create("trace.out")
trace.Start(f)
defer trace.Stop()
// Run code
// View: go tool trace trace.outFundamentos de CGO
CGO Básico
CGO habilita llamar a C desde Go. El código C va en comentarios encima de import "C". El import debe estar inmediatamente después del comentario. CGO ralentiza los builds e impide la cross-compilación.
/*
#include <stdio.h>
void hello() {
printf("Hello from C!\n");
}
*/
import "C"
func main() {
C.hello()
}Pasar Cadenas
C.CString asigna una cadena C (debes liberar con C.free). CString copia los datos. Usa unsafe.Pointer para conversión. Libera siempre la memoria asignada en C para evitar fugas.
/*
#include <string.h>
int len(const char* s) { return strlen(s); }
*/
import "C"
import "unsafe"
s := "Hello"cs := C.CString(s)
defer C.free(unsafe.Pointer(cs))
length := C.len(cs)Llamar a Librerías C
#cgo LDFLAGS enlaza librerías C. #cgo CFLAGS establece flags del compilador. Incluye cabeceras del sistema con #include. CGO tiende un puente entre Go y librerías C existentes como libm, libcrypto.
/*
#cgo LDFLAGS: -lm
#include <math.h>
*/
import "C"
result := float64(C.sqrt(16.0))
fmt.Println(result) // 4Callback de C a Go
Las funciones de Go exportadas con //export pueden llamarse desde C. La función debe estar en el paquete main. Habilita que librerías C llamen de vuelta a Go. Usado en bindings FFI.
/*
extern void goCallback(int);
void callGo(int n) { goCallback(n); }
*/
import "C"
//export goCallback
func goCallback(n C.int) {
fmt.Printf("Called with %d\n", n)
}Notas de Rendimiento
Las llamadas CGO tienen sobrecarga significativa comparadas con las llamadas a funciones de Go. Evítalas en código crítico de rendimiento. Agrupa operaciones para reducir llamadas cross-boundary. Establece CGO_ENABLED=0 para builds puros de Go.
// CGO calls have overhead (~100ns)
// Avoid in hot paths
// Batch C calls to amortize cost
// CGO disables inlining and some optimizations
// Build: CGO_ENABLED=1 go buildFrameworks Web
Servidor net/http
net/http es el servidor HTTP de la biblioteca estándar. HandleFunc registra handlers. ListenAndServe inicia el servidor. El mux por defecto está bien para apps simples; usa un mux personalizado para producción.
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
})
http.HandleFunc("/api", apiHandler)
log.Fatal(http.ListenAndServe(":8080", nil))Framework Gin
Gin es un framework HTTP de alto rendimiento. Param extrae parámetros de URL. JSON serializa respuestas. Gin proporciona routing, middleware y validación JSON. Más rápido que net/http gracias a httprouter.
import "github.com/gin-gonic/gin"
r := gin.Default()
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{"user": id})
})
r.Run(":8080")Middleware
El middleware envuelve handlers para añadir concerns transversales: logging, auth, CORS. c.Next() llama al siguiente handler. gin.Recovery() previene cuelgues por panics. El orden importa.
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
fmt.Printf("%s %s %v\n", c.Request.Method, c.URL.Path, time.Since(start))
}
}
r := gin.New()
r.Use(Logger(), gin.Recovery())Framework Echo
Echo es otro framework popular, similar a Gin. Los handlers devuelven errores para manejo centralizado de errores. Middleware integrado para CORS, JWT, rate limiting. Diseño de API limpio.
import "github.com/labstack/echo/v4"
e := echo.New()
e.GET("/users/:id", func(c echo.Context) error {
id := c.Param("id")
return c.JSON(200, map[string]string{"user": id})
})
e.Start(":8080")Archivos Estáticos
FileServer sirve archivos estáticos. StripPrefix ajusta la ruta URL. Útil para servir HTML, CSS, JS e imágenes. Para producción, usa un CDN o nginx para assets estáticos.
// net/http
fs := http.FileServer(http.Dir("./static"))
http.Handle("/static/", http.StripPrefix("/static/", fs))
// Gin
r.Static("/assets", "./assets")
r.StaticFile("/favicon.ico", "./favicon.ico")gRPC
Definir Proto
Protocol Buffers define el contrato del servicio. proto3 es la sintaxis más reciente. service define métodos RPC. message define estructuras de datos. Genera código Go con protoc.
syntax = "proto3";
package greet;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest { string name = 1; }
message HelloReply { string message = 1; }Generar Código
protoc genera código Go desde archivos .proto. --go_out genera tipos de mensaje. --go-grpc_out genera stubs de servicio. El código generado no se edita manualmente.
# Install protoc and plugins
protoc --go_out=. --go-grpc_out=. \
greet.proto
# This generates greet.pb.go and greet_grpc.pb.go
# Containing message types and service interfacesImplementación del Servidor
Embebe UnimplementedGreeterServer para compatibilidad hacia adelante. Implementa los métodos del servicio. grpc.NewServer crea el servidor. Registra el servicio antes de servir.
type server struct { greet.UnimplementedGreeterServer }
func (s *server) SayHello(ctx context.Context, in *greet.HelloRequest) (*greet.HelloReply, error) {
return &greet.HelloReply{Message: "Hello " + in.Name}, nil
}
lis, _ := net.Listen("tcp", ":50051")
grpc.NewServer().Serve(lis)Cliente
grpc.Dial establece una conexión. WithInsecure deshabilita TLS (usa credentials.NewTLS para producción). El stub del cliente proporciona métodos tipados. Las conexiones se pool y reutilizan.
conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
client := greet.NewGreeterClient(conn)
resp, _ := client.SayHello(context.Background(), &greet.HelloRequest{Name: "Alice"})
fmt.Println(resp.Message)Streaming
gRPC admite tres patrones de streaming. La palabra clave stream marca el streaming. Server streaming: una petición, muchas respuestas. Bidireccional: ambos lados hacen stream. Útil para datos en tiempo real.
// Server streaming
rpc LotsOfReplies(HelloRequest) returns (stream HelloReply);
// Client streaming
rpc LotsOfGreetings(stream HelloRequest) returns (HelloReply);
// Bidirectional
rpc BidiHello(stream HelloRequest) returns (stream HelloReply);Wrapping de Errores
Envolver Errores
Usa el verbo %w para envolver errores, preservando el original. Esto crea una cadena de errores. Evita %v para envolver ya que pierde la cadena. Envolver añade contexto sin perder la causa raíz.
if err != nil {
return fmt.Errorf("failed to open config: %w", err)
}Desenvolver
errors.Is comprueba si algún error de la cadena coincide. errors.As extrae un tipo de error específico de la cadena. Usa Is para valores centinela, As para errores tipados. Ambos recorren la cadena de wrap.
err := someOperation()
if errors.Is(err, sql.ErrNoRows) {
// Handle not found
}
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
fmt.Println("Path:", pathErr.Path)
}Errores Personalizados
Los tipos de error personalizados implementan la interfaz error. Transportan datos estructurados para manejo de errores. Usa errors.As para extraer el tipo personalizado. Prefiere errores tipados sobre comparaciones de cadenas.
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Msg)
}
func validate(s string) error {
if s == "" { return &ValidationError{Field: "name", Msg: "required"} }
return nil
}Errores Centinela
Los errores centinela son variables de error a nivel de paquete. Úsalos para condiciones de error esperadas. Comprueba con errors.Is, nunca con ==. Expórtalos para que los usuarios los comprueben.
var ErrNotFound = errors.New("not found")
func Find(id int) (*User, error) {
if id > 100 { return nil, ErrNotFound }
return &User{}, nil
}
// Check with errors.Is
if errors.Is(err, ErrNotFound) { /* ... */ }Patrones de Manejo de Errores
Maneja los errores inmediatamente cuando sea posible. Para operaciones diferidas como Close, captura el error. Los valores de retorno con nombre permiten a las funciones diferidas modificar el valor de retorno. Comprueba siempre los errores de Close.
// Immediate handling
if err := doSomething(); err != nil {
return fmt.Errorf("operation failed: %w", err)
}
// Deferred error checking (e.g., Close)
func readFile() (err error) {
f, e := os.Open("file.txt")
if e != nil { return e }
defer func() {
if cerr := f.Close(); err == nil { err = cerr }
}()
// ...
}Errores Comunes
Fugas de Goroutines
Las goroutines se filtran cuando bloquean para siempre. Proporciona siempre una salida: cancelación de context, cierre de canales o canales con búfer. Usa runtime.NumGoroutine() para detectar fugas.
// BUG: goroutine leaks if receiver stops
func send(ch chan int) {
go func() { ch <- 1 }() // Blocks forever
}
// FIX: use context or buffered channel
func send(ctx context.Context, ch chan int) {
go func() {
select {
case ch <- 1:
case <-ctx.Done():
}
}()
}Cierre de Canal
Solo el emisor debería cerrar un canal, nunca el receptor. Cerrar señaliza que no hay más valores. Recibir de un canal cerrado devuelve el valor cero. Enviar a un canal cerrado entra en pánico.
// Only the sender should close a channel
// Closing from receiver causes panic
ch := make(chan int)
go func() {
defer close(ch) // Sender closes
for i := 0; i < 5; i++ { ch <- i }
}()
for v := range ch { fmt.Println(v) }Captura de Variable de Bucle
Antes de Go 1.22, las variables de bucle se compartían entre iteraciones. Las goroutines que las capturan ven el valor final. Go 1.22+ lo arregla creando una variable nueva por iteración. Pasa como parámetro para versiones anteriores.
// BUG (Go < 1.22): all goroutines see last value
for i := 0; i < 3; i++ {
go func() { fmt.Println(i) }() // Prints 3,3,3
}
// FIX: pass as parameter
for i := 0; i < 3; i++ {
go func(i int) { fmt.Println(i) }(i)
}Concurrencia de Maps
Los maps no son seguros para uso concurrente. Las lecturas y escrituras concurrentes causan un pánico en runtime. Usa sync.Mutex para locking explícito o sync.Map para acceso concurrente con muchas lecturas.
// BUG: concurrent map writes panic
m := map[int]int{}
go func() { m[1] = 1 }()
go func() { m[2] = 2 }()
// FIX: use sync.Map or mutex
var mu sync.Mutex
mu.Lock(); m[1] = 1; mu.Unlock()Interfaz Nil
Un puntero nil envuelto en una interfaz no es nil. La interfaz tiene un tipo aunque el valor sea nil. Devuelve siempre nil directamente, no un puntero nil tipado. Comprueba con reflect o devuelve nil explícitamente.
// BUG: nil check fails
type MyError struct{}
func (e *MyError) Error() string { return "err" }
func doSomething() error {
var err *MyError = nil
return err // Non-nil interface!
}
// FIX: return nil explicitly
func doSomething() error {
return nil
}Fragmentos de Go relacionados
Copy-paste ready code for common tasks.
goroutine
Implementar concurrencia usando goroutines.
channel
Comunicarse entre goroutines usando channels.
select
Multiplexar channels usando select.
Mutex mutex
Proteger datos compartidos usando sync.Mutex.
Llamada Diferida con defer
Uso y orden de ejecución de defer.
Manejo de error
Patrón de manejo de errores de Go.
interface
Definir e implementar interfaces.
Embedding de Structs
Implementar composición vía embedding.
Genéricos
Uso de genéricos en Go 1.18+.
context
Controlar timeout y cancelación usando context.
Operaciones de Archivos
Operaciones de lectura y escritura de archivos.
Servidor HTTP
Crear un servidor HTTP.
Cliente HTTP
Enviar peticiones HTTP.
Codificación/Decodificación JSON
Convertir entre structs y JSON.
Procesamiento de Strings
Operaciones comunes en el paquete strings.
Operaciones con Slices
Operaciones comunes de slices.
Operaciones de map
Operaciones CRUD sobre map.
Manejo de Tiempo
Operaciones comunes en el paquete time.
Expresiones Regulares
Uso del paquete regexp.
Testing
Escribir pruebas unitarias.
Was this helpful?