cv.cm

Programación · Lenguajes de programación

Tutorial de Go para principiantes

7 min de lectura

Respuesta corta

Go (a menudo llamado Golang) es un lenguaje sencillo, compilado y de tipado estático creado por Google, pensado para compilaciones rápidas, concurrencia fácil y servidores fiables. Instala Go, escribe un paquete main y ejecútalo con go run hello.go. Las ideas centrales son un conjunto pequeño de tipos, slices y maps, structs con métodos, interfaces implícitas, goroutines y canales para la concurrencia, y errores devueltos como valores ordinarios.

Abrir Formateador JSON

Qué es Go y para qué se usa

Go fue diseñado en Google por Robert Griesemer, Rob Pike y Ken Thompson, y se publicó como código abierto en 2009. Compila a un único binario estático, arranca rápido, tiene recolector de basura y una rica biblioteca estándar para redes. Se usa en infraestructura en la nube y herramientas de línea de comandos: Docker, Kubernetes, Terraform y Prometheus están escritos en Go, y muchas empresas lo usan para APIs web y microservicios. El lenguaje es deliberadamente pequeño, por lo que el código de distintos equipos se parece.

Instalar Go y ejecutar Hello, world

Descarga Go desde go.dev/dl y compruébalo con go version. Crea una carpeta, inicia un módulo y añade main.go:

package main

import "fmt"

func main() {
    fmt.Println("Hello, world")
}
go mod init example.com/hello
go run .
# Hello, world
go build     # produces a standalone binary named hello

La ejecución empieza en func main dentro de package main. Ejecuta gofmt (o go fmt) para dar formato al código; todo el código Go usa el mismo estilo, con tabuladores para la indentación en los archivos reales.

Variables y tipos básicos

Declara variables con var name type = value, o dentro de funciones con la forma corta name := value, que infiere el tipo. Las variables a las que no se asigna nada reciben un valor cero: 0, "", false o nil.

TipoEjemploValor cero
int, int64n := 420
float64x := 3.140
boolok := truefalse
strings := "hi"""
runer := 'é'0 (un punto de código Unicode)
byteb := byte(65)0 (alias de uint8)
package main

import (
    "fmt"
    "strconv"
)

func main() {
    name := "Ada"
    count := 7
    avg := float64(count) / 2 // explicit conversion is required
    fmt.Printf("%s has %d items, avg %.1f\n", name, count, avg) // Ada has 7 items, avg 3.5
    n, err := strconv.Atoi("12")
    fmt.Println(n+1, err) // 13 <nil>
}

Go nunca convierte implícitamente entre tipos numéricos, y el compilador rechaza las variables y los imports sin usar.

Control de flujo y funciones

No hay paréntesis alrededor de las condiciones y las llaves son obligatorias. for es la única palabra clave de bucle: cubre los bucles clásicos, los de estilo while y los de rango. Las funciones pueden devolver varios valores.

package main

import "fmt"

func divmod(a, b int) (int, int) {
    return a / b, a % b
}

func main() {
    score := 72
    if score >= 90 {
        fmt.Println("A")
    } else if score >= 70 {
        fmt.Println("B") // B
    } else {
        fmt.Println("C")
    }

    for i := 0; i < 3; i++ {
        fmt.Print(i, " ") // 0 1 2
    }
    fmt.Println()

    n := 10
    for n > 1 { // while-style loop
        n /= 2
    }

    q, r := divmod(17, 5)
    fmt.Println(n, q, r) // 1 3 2

    switch day := "SAT"; day {
    case "SAT", "SUN":
        fmt.Println("weekend") // no break needed
    default:
        fmt.Println("weekday")
    }
}

Slices, maps y structs

Un slice es una vista ampliable de un array y es el tipo de lista de uso diario. Un map es una tabla hash. Un struct agrupa campos con nombre, y los métodos son funciones con un receptor.

package main

import (
    "fmt"
    "sort"
)

type Point struct {
    X, Y int
}

func (p Point) Add(o Point) Point { return Point{p.X + o.X, p.Y + o.Y} }

func (p *Point) Scale(k int) { p.X *= k; p.Y *= k } // pointer receiver modifies p

func main() {
    nums := []int{3, 1, 2}
    nums = append(nums, 5)
    sort.Ints(nums)
    fmt.Println(nums, len(nums), nums[1:3]) // [1 2 3 5] 4 [2 3]

    ages := map[string]int{"ann": 31, "bob": 25}
    ages["cy"] = 40
    age, ok := ages["zed"]
    fmt.Println(age, ok, len(ages)) // 0 false 3

    for i, n := range nums {
        fmt.Print(i, ":", n, " ") // 0:1 1:2 2:3 3:5
    }
    fmt.Println()

    p := Point{1, 2}.Add(Point{3, 4})
    p.Scale(2)
    fmt.Printf("%+v\n", p) // {X:8 Y:12}
}

El orden de iteración de un map es aleatorio a propósito; ordena las claves si necesitas un orden estable. Los nombres que empiezan con mayúscula (Point, Add) se exportan desde el paquete.

Interfaces

Una interfaz enumera firmas de métodos. Un tipo satisface una interfaz simplemente por tener esos métodos; no existe la palabra clave implements.

package main

import (
    "fmt"
    "math"
)

type Shape interface {
    Area() float64
}

type Circle struct{ R float64 }
type Rect struct{ W, H float64 }

func (c Circle) Area() float64 { return math.Pi * c.R * c.R }
func (r Rect) Area() float64   { return r.W * r.H }

func main() {
    shapes := []Shape{Circle{1}, Rect{2, 3}}
    total := 0.0
    for _, s := range shapes {
        total += s.Area()
    }
    fmt.Printf("%.2f\n", total) // 9.14
}

Go también tiene genéricos (desde Go 1.18), por ejemplo func Max[T int | float64](a, b T) T, que usarás sobre todo a través de funciones de biblioteca como las de los paquetes slices y maps.

Característica distintiva: goroutines y canales

Una goroutine es un hilo ligero que se inicia con la palabra clave go; un programa puede ejecutar miles de ellas. Las goroutines se comunican mediante canales, que pasan valores de forma segura entre ellas. sync.WaitGroup espera a que termine un grupo de goroutines.

package main

import (
    "fmt"
    "sync"
)

func square(n int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    results <- n * n
}

func main() {
    results := make(chan int, 5)
    var wg sync.WaitGroup
    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go square(i, results, &wg)
    }
    wg.Wait()
    close(results)

    sum := 0
    for r := range results {
        sum += r
    }
    fmt.Println(sum) // 55
}

defer programa una llamada para que se ejecute cuando retorne la función que la contiene, que es como el código Go cierra archivos y libera mutex. Usa go run -race . para detectar condiciones de carrera durante el desarrollo.

Manejo de errores

Go no tiene excepciones para los fallos ordinarios. Una función que puede fallar devuelve un error como último resultado, y quien la llama lo comprueba enseguida. Envuelve los errores con %w para añadir contexto y mantener el error original inspeccionable con errors.Is.

package main

import (
    "errors"
    "fmt"
    "os"
)

func readConfig(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("read config: %w", err)
    }
    return data, nil
}

func main() {
    _, err := readConfig("missing.json")
    if errors.Is(err, os.ErrNotExist) {
        fmt.Println("no config file:", err)
    }
}

panic y recover existen, pero se reservan para errores de programación y estados realmente inesperados, no para el manejo normal de errores.

Un programa completo pequeño: un servidor web mínimo

Con solo la biblioteca estándar, este programa sirve JSON por HTTP. Ejecútalo con go run . y abre http://localhost:8080/hello?name=Ann.

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "time"
)

type Greeting struct {
    Message string    `json:"message"`
    Time    time.Time `json:"time"`
}

func hello(w http.ResponseWriter, r *http.Request) {
    name := r.URL.Query().Get("name")
    if name == "" {
        name = "world"
    }
    w.Header().Set("Content-Type", "application/json")
    err := json.NewEncoder(w).Encode(Greeting{"Hello, " + name, time.Now()})
    if err != nil {
        log.Println("encode:", err)
    }
}

func main() {
    http.HandleFunc("/hello", hello)
    log.Println("listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

El texto que sigue al tipo de cada campo es una etiqueta de struct (struct tag) que fija el nombre de la clave JSON. Cada petición se atiende automáticamente en su propia goroutine.

Errores comunes de principiantes

  • Ignorar errores con _. Comprueba cada error devuelto, salvo que estés seguro de que no puede importar.
  • Esperar que append modifique el slice original en el sitio; usa siempre el resultado, como en s = append(s, x).
  • Iniciar goroutines y dejar que main retorne antes de que terminen. Espera con sync.WaitGroup o con canales.
  • Usar un receptor por valor cuando el método debe modificar el struct; usa un receptor puntero como func (p *Point).

Adónde ir después

Haz el Tour of Go interactivo y lee después Effective Go y la documentación de la biblioteca estándar en pkg.go.dev. Aprende go test para pruebas unitarias y benchmarks, el paquete context para tiempos de espera y cancelación, y los módulos para las dependencias.

Preguntas frecuentes

¿Se llama Go o Golang?

El nombre oficial es Go. «Golang» viene de la antigua dirección web golang.org y se sigue usando como término de búsqueda.

¿Go es orientado a objetos?

En parte. Go tiene métodos e interfaces, pero no clases ni herencia; los tipos se construyen incrustando structs y componiendo interfaces pequeñas.

¿Go es más rápido que Python?

En la mayoría de las tareas limitadas por CPU, sí, porque Go compila a código máquina nativo y tiene tipos estáticos. Python aún puede ser rápido cuando el trabajo pesado ocurre dentro de bibliotecas escritas en C.

¿Cuál es la diferencia entre una goroutine y un hilo?

Una goroutine la gestiona el entorno de ejecución de Go y empieza con una pila pequeña que crece según haga falta. El entorno de ejecución planifica muchas goroutines sobre unos pocos hilos del sistema operativo, por lo que crear goroutines es mucho más barato.

Todos los tutoriales de programación