DEV Community

Cover image for Fundamentos de GO
Lucatonny Raudales
Lucatonny Raudales

Posted on

Fundamentos de GO

Eres nuevo en Golang?, tienes una entrevista y quieres repasar los fundamentos del lenguaje?, o simplemente quieres ver algo interesante mientras mirás Harry Potter?...

Bien, hace poco estuve repasando los fundamentos de Go porque, seamos sinceros, a veces se nos olvida hasta de comer con tanta información que manejamos día a día.
Repasemos algunos fundamentos de golang que vinieron a mi cabeza:

  1. Interfaces y composición en Go.
  2. Manejo de errores (errors.Wrap, fmt.Efforf, etc.).
  3. Goroutines y canales (¿cuando usarlos?, ¿cuando no usarlos?).
  4. Uso de context (context.WithTimeout, cancelación de requests).
  5. Struct tags (json, bson, validación con go-playground/validator).
  6. Testeo: testing, testify, mocks.

Voy a explicar cada uno de los items anteriores como si estuviéramos hablando en una llamada o en una salida al mcDonald, con ejemplos claros y al grano.

Interfaces y Composición

¿Qué carajos es una interfaz en GO?
Una interfaz es básicamente un contrato.
Te dice qué estructura, métodos o componentes debe tener un tipo, pero no te dice como hacer las cosas.

type Animal interface {
    Speak() string
}
Enter fullscreen mode Exit fullscreen mode

Ahora, cualquier struct que tenga ese método, ya cumple con los mandatos de la interfaz. No tenés que decir explícitamente implement Animal, Go lo deduce solo porque es muy inteligente, no como otros lenguajes que no quiero decir (Typescript, etc).

type Dog struct{}

func (d Dog) Speak() string {
    return "Guau!"
}
Enter fullscreen mode Exit fullscreen mode

uso

func makeItSpeak(a Animal) {
    fmt.Println(a.Speak())
}
Enter fullscreen mode Exit fullscreen mode

¿Y composición?
Go no tiene herencia como en otros lenguajes (gracias a Dios), pero podés componer structs. En español sería como meter un struct dentro de otro (como structs anidados)

type Kitchen struct {
    Color string
    Windows int
}

type Home struct {
    Kitchen // composición
    Rooms  int
}
Enter fullscreen mode Exit fullscreen mode

uso

func main() {
    h := Home{Kitchen{"blue", 1}, 3}
    fmt.Println(h.Kitchen.Color) // "blue"
    fmt.Println(h.Kitchen.Windows) // 1
    fmt.Println(h.Rooms) // 3
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Manejo de errores (Como gente seria)
Standby

Go no tiene try/catch, acá se devuelve el error en cada acción(o en casi todas) y vos hacés con el error lo que tú quieras.

func dividir(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("no se puede dividir por cero")
    }
    return a / b, nil
}

res, err := dividir(10, 0)
if err != nil {
    fmt.Println("Ups:", err)
}

Enter fullscreen mode Exit fullscreen mode

¿Y si quiero más contexto?
Para eso está errors.Wrap de la librería pkg/errors, para decir no solo lo que pasó, sino también dónde ocurrió.

import "github.com/pkg/errors"

func readFile(path string) error {
    _, err := os.ReadFile(path)
    if err != nil {
        return errors.Wrap(err, "falló al leer el archivo")
    }
    return nil
}

Enter fullscreen mode Exit fullscreen mode

realmente hay otras librerías que nos ayudan con esto y más, y de maneras distintas, pero para gusto los colores.

🚀 Goroutines y canales (concurrency sin drama)

Go routines

¿Qué es una goroutine?
En Go, si querés que algo corra en paralelo mientras sigue ejecutándose el resto del programa, usás una goroutine.
Es como lanzar una función en segundo plano, pero mucho más liviano que un thread traidicional. Se lanza con la palabra reservada go.

func saludar(nombre string) {
    fmt.Println("Hola", nombre)
}

func main() {
    go saludar("Lucatonny") // esto se ejecuta en segundo plano
    fmt.Println("Programa principal sigue...")
    time.Sleep(1 * time.Second) // damos tiempo a que saludar termine
}

Enter fullscreen mode Exit fullscreen mode

Si no ponés el Sleep, puede que la goroutine ni se alcance a ejecutar ya que el programa puede terminarse antes.

¿Y los canales?
Un canal (chan) sirve para que dos goroutines se pasen datos de forma segura. es como comunicarse entre multiversos así como el doctor Strange.

func enviar(ch chan string) {
    ch <- "Hola desde la goroutine"
}

func main() {
    ch := make(chan string)
    go enviar(ch)
    msg := <-ch
    fmt.Println(msg)
}
Enter fullscreen mode Exit fullscreen mode
  • ch <- "Hola": envía al canal.
  • mensage := <-ch: recibe del canal.

Con esto evitás tener que usar locks o variables compartidas.

Ejemplo real: múltiples workers

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Printf("Worker %d procesando trabajo %d\n", id, j)
        time.Sleep(time.Second)
        results <- j * 2
    }
}

func main() {
    jobs := make(chan int, 5)
    results := make(chan int, 5)

    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs)

    for r := 1; r <= 5; r++ {
        fmt.Println("Resultado:", <-results)
    }
}
Enter fullscreen mode Exit fullscreen mode

y ¿Cuando no usarlas?

  • Si necesitás un resultado inmediato (no paralelo).
  • Si no estás seguro de como coordinarlas (puede haber deadlocks).
  • Si vas a lanzar cientos sin control -> consumo de memoria innecesario.
  • si no hay un plan para cerrarlas (se pueden quedar vivas para siempre y consumir recursos innecesariamente hasta que explotes).

Uso de context (controlá tus procesos)
Context
context es tu mejor amigo cuando hacés operaciones que podrían tardar mucho (como llamadas a APIs, DB, procesamiento pesado, etc.)

Te permite:

  • Cancelar procesos si se extienden del tiempo.
  • Cancelar cuando el usuario se va.
  • Encadenar procesos con timeout.
func hacerAlgo(ctx context.Context) {
    select {
    case <-time.After(3 * time.Second):
        fmt.Println("Proceso completado")
    case <-ctx.Done():
        fmt.Println("Cancelado:", ctx.Err())
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    hacerAlgo(ctx)
}
Enter fullscreen mode Exit fullscreen mode

En este caso, el proceso se cancela a los 2 segundos.

Context también se usa así:

req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.com", nil)
Enter fullscreen mode Exit fullscreen mode

En servicios HTTP o gRPC, usar context es esencial para evitar que las goroutines queden abiertas esperando una respuesta que ya no importa.

Struct tags (los metadatos mágicos)
Los struct tagsson anotaciones que van junto al campo de un struct. Le dicen a Go (y a otras librerías): "Ey, tratá este campo así"

type Usuario struct {
    Nombre string `json:"nombre"`
    Edad   int    `json:"edad"`
}

u := Usuario{"Lucatonny", 300}
jsonData, _ := json.Marshal(u)
fmt.Println(string(jsonData)) // {"nombre":"Lucatonny","edad":300}
Enter fullscreen mode Exit fullscreen mode

Si no ponés el tag, json.Marshal usaría Nombre y Edad, respetando las mayúsculas.

Validaciones con go-playground/validator
Es un paquete que nos ayuda a validar los valores de entrada y salida en cada campo que hemos definido en nuestro estruct.

type Registro struct {
    Email string `validate:"required,email"`
    Edad  int    `validate:"gte=18"`
}

validate := validator.New()
err := validate.Struct(Registro{})
if err != nil {
    fmt.Println("Error de validación:", err)
}
Enter fullscreen mode Exit fullscreen mode

Te ahorra mucho código repetitivo para validar formularios, APIs, etc.

Testing (como pro y sin miedo)
Test básico

func Sumar(a, b int) int {
    return a + b
}
Enter fullscreen mode Exit fullscreen mode
// archivo: sumar_test.go
func TestSumar(t *testing.T) {
    resultado := Sumar(2, 3)
    if resultado != 5 {
        t.Errorf("esperaba 5, obtuve %d", resultado)
    }
}
Enter fullscreen mode Exit fullscreen mode

Con testify todo es más fluido

import (
    "testing"
    "github.com/stretchr/testify/assert"
)

func TestSumar(t *testing.T) {
    assert.Equal(t, 5, Sumar(2, 3))
}
Enter fullscreen mode Exit fullscreen mode

¿Y si quiero mocks?
Supongamos que tenés la siguiente interfaz:

type EmailSender interface {
    Send(email string, content string) error
}
Enter fullscreen mode Exit fullscreen mode

Podés crear un mock con testify/mock:

type MockSender struct {
    mock.Mock
}

func (m *MockSender) Send(email string, content string) error {
    args := m.Called(email, content)
    return args.Error(0)
}
Enter fullscreen mode Exit fullscreen mode

y en el test tener:

func TestEnviarCorreo(t *testing.T) {
    m := new(MockSender)
    m.On("Send", "test@example.com", "hola").Return(nil)

    // Llamás a la función que usa EmailSender
    err := m.Send("test@example.com", "hola")
    assert.NoError(t, err)
    m.AssertExpectations(t)
}
Enter fullscreen mode Exit fullscreen mode

Los mocks te permiten probar sin depender de servicios externos. Ideal en integración.

En Resumen
Go tiene esa belleza de ser simple, pero no simplista. Con estos fundamentos ya podés hacer APIs, workers, microservicios, CLI tools, y lo que se te ocurra. Lo importante no es solo saber qué hacen estas herramientas, sino cuándo usarlas y por qué.

Si algo de esto te sirvió, te aclaró o incluso te hizo decir “¡ahhh ahora entiendo!”, entonces ya valió la pena.

¿Querés ver más artículos como este? ¿Querés que arme una segunda parte con patrones, middlewares, caché, gRPC o integración con bases de datos?
Dejame un comentario o escribime por X (Twitter).

Y si recién estás empezando en Go: no te frustres si al principio parece raro. Todos pasamos por eso. Solo seguí escribiendo código, y un día te vas a dar cuenta de que ya pensás en Go.

Saludos y hasta pronto.
Lucatonny R. Raudales

Top comments (0)