Cristhian Villegas
Backend10 min read1 views

Structs, Metodos e Interfaces en Go: Guia Completa

Structs, Metodos e Interfaces en Go: Guia Completa

Logo de Go

Introduccion: tipos personalizados en Go

Bienvenido a la parte 6 de nuestro curso de Go para principiantes. En los articulos anteriores aprendimos a usar los tipos basicos y las colecciones de datos. Pero en la vida real necesitamos representar entidades complejas como usuarios, productos o vehiculos, con multiples propiedades relacionadas.

Go no tiene clases como Java o Python, pero tiene algo igual de poderoso: structs, metodos e interfaces. Juntos, estos tres conceptos te permiten escribir codigo organizado, reutilizable y flexible. En este articulo aprenderemos como Go aborda la programacion orientada a objetos de una manera unica y elegante.

Structs: definicion e inicializacion

Un struct es un tipo de dato compuesto que agrupa campos (propiedades) bajo un mismo nombre. Es el equivalente a una clase en otros lenguajes, pero sin herencia ni constructores magicos.

Definir y crear structs

go
1package main
2
3import "fmt"
4
5// Definir un struct
6type Persona struct {
7    Nombre string
8    Edad   int
9    Email  string
10}
11
12func main() {
13    // Forma 1: inicializar con campos nombrados (recomendado)
14    p1 := Persona{
15        Nombre: "Ana Lopez",
16        Edad:   25,
17        Email:  "[email protected]",
18    }
19    fmt.Println(p1) // {Ana Lopez 25 [email protected]}
20
21    // Forma 2: inicializar en orden (no recomendado, fragil)
22    p2 := Persona{"Luis Garcia", 30, "[email protected]"}
23    fmt.Println(p2)
24
25    // Forma 3: struct vacio (campos con valores cero)
26    var p3 Persona
27    fmt.Println(p3)         // { 0 }
28    fmt.Println(p3.Nombre)  // "" (string vacio)
29    fmt.Println(p3.Edad)    // 0
30
31    // Acceder y modificar campos
32    p3.Nombre = "Maria"
33    p3.Edad = 28
34    fmt.Printf("%s tiene %d anos\n", p3.Nombre, p3.Edad)
35}
Tip: Siempre usa la inicializacion con campos nombrados (Nombre: "Ana") en lugar de la posicional. Si en el futuro agregas un campo al struct, el codigo posicional se rompe pero el nombrado sigue funcionando.

Punteros a structs

En Go, los structs se pasan por valor (se hace una copia). Si quieres modificar el struct original, necesitas un puntero:

go
1package main
2
3import "fmt"
4
5type Producto struct {
6    Nombre string
7    Precio float64
8}
9
10func aplicarDescuento(p *Producto, porcentaje float64) {
11    p.Precio = p.Precio * (1 - porcentaje/100)
12}
13
14func main() {
15    laptop := Producto{Nombre: "Laptop Pro", Precio: 1500.00}
16    fmt.Printf("Antes: $%.2f\n", laptop.Precio) // $1500.00
17
18    aplicarDescuento(&laptop, 10)
19    fmt.Printf("Despues: $%.2f\n", laptop.Precio) // $1350.00
20
21    // Crear con puntero directamente
22    tablet := &Producto{Nombre: "Tablet X", Precio: 500.00}
23    aplicarDescuento(tablet, 20)
24    fmt.Printf("Tablet: $%.2f\n", tablet.Precio) // $400.00
25}
Nota: Go automaticamente desreferencia los punteros a structs. Puedes escribir p.Nombre en lugar de (*p).Nombre, lo cual hace el codigo mas limpio.

Structs anidados y campos anonimos (embedding)

Go permite anidar structs dentro de otros y usar campos anonimos para lograr algo similar a la herencia. Esto se llama embedding y es una de las caracteristicas mas poderosas de Go.

go
1package main
2
3import "fmt"
4
5// Struct base
6type Direccion struct {
7    Calle  string
8    Ciudad string
9    Estado string
10}
11
12// Struct con campo nombrado (composicion explicita)
13type Empleado struct {
14    Nombre    string
15    Puesto    string
16    Direccion Direccion // campo nombrado
17}
18
19// Struct con campo anonimo (embedding)
20type Cliente struct {
21    Nombre string
22    Email  string
23    Direccion // campo anonimo - se "hereda" la estructura
24}
25
26func main() {
27    // Con campo nombrado: acceso explícito
28    emp := Empleado{
29        Nombre: "Carlos",
30        Puesto: "Desarrollador",
31        Direccion: Direccion{
32            Calle:  "Av. Reforma 100",
33            Ciudad: "CDMX",
34            Estado: "CDMX",
35        },
36    }
37    fmt.Println(emp.Direccion.Ciudad) // CDMX
38
39    // Con embedding: acceso directo a los campos
40    cli := Cliente{
41        Nombre: "Maria",
42        Email:  "[email protected]",
43        Direccion: Direccion{
44            Calle:  "Calle Luna 50",
45            Ciudad: "Guadalajara",
46            Estado: "Jalisco",
47        },
48    }
49    // Acceso directo (Go "promueve" los campos)
50    fmt.Println(cli.Ciudad)           // Guadalajara
51    fmt.Println(cli.Direccion.Ciudad) // Guadalajara (tambien funciona)
52}
Tip: El embedding no es herencia. Es composicion. El struct embebido no "es" un tipo padre, simplemente sus campos y metodos se promueven al struct contenedor. Esto es intencional en Go: composicion sobre herencia.

Metodos: value receivers vs pointer receivers

En Go puedes asociar funciones a un tipo mediante metodos. Un metodo es simplemente una funcion con un receiver (receptor) que indica sobre que tipo opera.

Value receiver vs pointer receiver

go
1package main
2
3import "fmt"
4
5type Rectangulo struct {
6    Ancho float64
7    Alto  float64
8}
9
10// Value receiver: trabaja con una COPIA del struct
11// Usa esto cuando solo necesitas LEER datos
12func (r Rectangulo) Area() float64 {
13    return r.Ancho * r.Alto
14}
15
16// Value receiver: no modifica el original
17func (r Rectangulo) Perimetro() float64 {
18    return 2 * (r.Ancho + r.Alto)
19}
20
21// Pointer receiver: trabaja con el struct ORIGINAL
22// Usa esto cuando necesitas MODIFICAR datos
23func (r *Rectangulo) Escalar(factor float64) {
24    r.Ancho *= factor
25    r.Alto *= factor
26}
27
28func main() {
29    rect := Rectangulo{Ancho: 10, Alto: 5}
30
31    fmt.Printf("Area: %.2f\n", rect.Area())         // 50.00
32    fmt.Printf("Perimetro: %.2f\n", rect.Perimetro()) // 30.00
33
34    // Escalar modifica el struct original
35    rect.Escalar(2)
36    fmt.Printf("Despues de escalar: %v\n", rect) // {20 10}
37    fmt.Printf("Nueva area: %.2f\n", rect.Area())  // 200.00
38}
Regla general: Si el metodo modifica el struct, usa pointer receiver (*Tipo). Si solo lee datos, usa value receiver (Tipo). Si tienes duda, usa pointer receiver. Tambien, si algun metodo del tipo usa pointer receiver, por consistencia todos deberian usarlo.

Interfaces: implementacion implicita

Una interface en Go define un conjunto de metodos que un tipo debe implementar. La gran diferencia con otros lenguajes es que la implementacion es implicita: no necesitas escribir implements ni ninguna declaracion especial.

go
1package main
2
3import (
4    "fmt"
5    "math"
6)
7
8// Definir una interface
9type Figura interface {
10    Area() float64
11    Perimetro() float64
12}
13
14// Circulo implementa Figura (implicitamente)
15type Circulo struct {
16    Radio float64
17}
18
19func (c Circulo) Area() float64 {
20    return math.Pi * c.Radio * c.Radio
21}
22
23func (c Circulo) Perimetro() float64 {
24    return 2 * math.Pi * c.Radio
25}
26
27// Rectangulo implementa Figura (implicitamente)
28type Rectangulo struct {
29    Ancho float64
30    Alto  float64
31}
32
33func (r Rectangulo) Area() float64 {
34    return r.Ancho * r.Alto
35}
36
37func (r Rectangulo) Perimetro() float64 {
38    return 2 * (r.Ancho + r.Alto)
39}
40
41// Funcion que acepta cualquier Figura
42func imprimirInfo(f Figura) {
43    fmt.Printf("Area: %.2f\n", f.Area())
44    fmt.Printf("Perimetro: %.2f\n", f.Perimetro())
45}
46
47func main() {
48    c := Circulo{Radio: 5}
49    r := Rectangulo{Ancho: 10, Alto: 3}
50
51    fmt.Println("=== Circulo ===")
52    imprimirInfo(c)
53
54    fmt.Println("\n=== Rectangulo ===")
55    imprimirInfo(r)
56
57    // Un slice de interfaces puede contener diferentes tipos
58    figuras := []Figura{
59        Circulo{Radio: 3},
60        Rectangulo{Ancho: 4, Alto: 6},
61        Circulo{Radio: 7},
62    }
63
64    fmt.Println("\n=== Todas las figuras ===")
65    for _, fig := range figuras {
66        fmt.Printf("Area: %.2f\n", fig.Area())
67    }
68}
Nota: Si un tipo tiene todos los metodos que define una interface, automaticamente la satisface. No hay necesidad de una declaracion explicita como class Circulo implements Figura. Esto hace que las interfaces en Go sean extremadamente flexibles.

Interface vacia y type assertions

La interface vacia (interface{} o any en Go 1.18+) puede contener cualquier valor. Es similar a Object en Java o any en TypeScript. Para extraer el tipo concreto, usamos type assertions y type switches.

go
1package main
2
3import "fmt"
4
5func describir(i interface{}) {
6    // Type switch: determinar el tipo concreto
7    switch v := i.(type) {
8    case int:
9        fmt.Printf("Entero: %d (el doble es %d)\n", v, v*2)
10    case string:
11        fmt.Printf("String: %q (longitud %d)\n", v, len(v))
12    case bool:
13        fmt.Printf("Booleano: %t\n", v)
14    case []int:
15        fmt.Printf("Slice de enteros: %v (longitud %d)\n", v, len(v))
16    default:
17        fmt.Printf("Tipo desconocido: %T = %v\n", v, v)
18    }
19}
20
21func main() {
22    describir(42)
23    describir("hola mundo")
24    describir(true)
25    describir([]int{1, 2, 3})
26    describir(3.14)
27
28    // Type assertion directa (con verificacion)
29    var valor interface{} = "Go es genial"
30
31    texto, ok := valor.(string)
32    if ok {
33        fmt.Println("Es un string:", texto)
34    }
35
36    // Esto fallaria en tiempo de ejecucion sin el check
37    numero, ok := valor.(int)
38    if !ok {
39        fmt.Println("No es un entero, valor cero:", numero)
40    }
41}
Atencion: Siempre usa el patron valor, ok := i.(Tipo) en lugar de valor := i.(Tipo). Sin el segundo valor, si el tipo no coincide, el programa entra en panico (panic) y se detiene abruptamente.

Interfaces comunes de la libreria estandar

Go tiene varias interfaces estandar que son fundamentales para el ecosistema. Conocerlas te hara mucho mas productivo.

Stringer y error

go
1package main
2
3import (
4    "errors"
5    "fmt"
6)
7
8// Implementar fmt.Stringer para personalizar la impresion
9type Producto struct {
10    Nombre string
11    Precio float64
12}
13
14func (p Producto) String() string {
15    return fmt.Sprintf("%s ($%.2f)", p.Nombre, p.Precio)
16}
17
18// Implementar la interface error
19type ErrorValidacion struct {
20    Campo   string
21    Mensaje string
22}
23
24func (e ErrorValidacion) Error() string {
25    return fmt.Sprintf("error de validacion en '%s': %s", e.Campo, e.Mensaje)
26}
27
28func validarEdad(edad int) error {
29    if edad < 0 {
30        return ErrorValidacion{Campo: "edad", Mensaje: "no puede ser negativa"}
31    }
32    if edad > 150 {
33        return ErrorValidacion{Campo: "edad", Mensaje: "valor no realista"}
34    }
35    return nil
36}
37
38func main() {
39    // Stringer: fmt.Println usa String() automaticamente
40    p := Producto{Nombre: "Laptop", Precio: 1299.99}
41    fmt.Println(p) // Laptop ($1299.99)
42
43    // error: manejar errores personalizados
44    if err := validarEdad(-5); err != nil {
45        fmt.Println("Error:", err)
46
47        // Verificar el tipo especifico del error
48        var ve ErrorValidacion
49        if errors.As(err, &ve) {
50            fmt.Println("Campo:", ve.Campo)
51            fmt.Println("Detalle:", ve.Mensaje)
52        }
53    }
54
55    if err := validarEdad(25); err == nil {
56        fmt.Println("Edad valida")
57    }
58}

io.Reader e io.Writer

go
1package main
2
3import (
4    "fmt"
5    "io"
6    "os"
7    "strings"
8)
9
10func main() {
11    // strings.NewReader implementa io.Reader
12    reader := strings.NewReader("Hola desde un Reader!")
13
14    // os.Stdout implementa io.Writer
15    // io.Copy conecta un Reader con un Writer
16    bytesCopiados, err := io.Copy(os.Stdout, reader)
17    if err != nil {
18        fmt.Println("Error:", err)
19        return
20    }
21    fmt.Printf("\n(Se copiaron %d bytes)\n", bytesCopiados)
22}

Composicion sobre herencia

Go no tiene herencia de clases. En su lugar, promueve la composicion: construir tipos complejos combinando tipos mas simples. Esto es una decision de diseno intencional que evita muchos problemas de la herencia clasica (como el problema del diamante).

go
1package main
2
3import "fmt"
4
5// Comportamientos como interfaces pequenas
6type Notificable interface {
7    Notificar(mensaje string)
8}
9
10type Almacenable interface {
11    Guardar() error
12}
13
14// Tipos base con funcionalidad especifica
15type NotificadorEmail struct {
16    Email string
17}
18
19func (n NotificadorEmail) Notificar(mensaje string) {
20    fmt.Printf("Enviando email a %s: %s\n", n.Email, mensaje)
21}
22
23type AlmacenDB struct {
24    Tabla string
25}
26
27func (a AlmacenDB) Guardar() error {
28    fmt.Printf("Guardando en tabla '%s'\n", a.Tabla)
29    return nil
30}
31
32// Componer un tipo complejo con embedding
33type Usuario struct {
34    Nombre string
35    NotificadorEmail // embedding: Usuario "hereda" Notificar()
36    AlmacenDB        // embedding: Usuario "hereda" Guardar()
37}
38
39func main() {
40    u := Usuario{
41        Nombre:           "Ana",
42        NotificadorEmail: NotificadorEmail{Email: "[email protected]"},
43        AlmacenDB:        AlmacenDB{Tabla: "usuarios"},
44    }
45
46    // Metodos promovidos: se usan directamente
47    u.Notificar("Bienvenida al sistema")
48    u.Guardar()
49
50    // Tambien satisface las interfaces
51    var n Notificable = u
52    n.Notificar("Recordatorio importante")
53
54    var a Almacenable = u
55    a.Guardar()
56}
Tip: La filosofia de Go es: "define interfaces pequenas con pocos metodos y compon tipos usando embedding". Interfaces de 1-2 metodos son la norma en Go. Esto hace que el codigo sea mas flexible y facil de testear.

Ejemplo practico: calculadora de figuras geometricas

Vamos a construir un programa completo que combina structs, metodos e interfaces para crear una calculadora de figuras geometricas.

go
1package main
2
3import (
4    "fmt"
5    "math"
6    "sort"
7)
8
9// Interface que deben cumplir todas las figuras
10type Figura interface {
11    Area() float64
12    Perimetro() float64
13    Nombre() string
14}
15
16// --- Circulo ---
17type Circulo struct {
18    Radio float64
19}
20
21func (c Circulo) Area() float64 {
22    return math.Pi * c.Radio * c.Radio
23}
24
25func (c Circulo) Perimetro() float64 {
26    return 2 * math.Pi * c.Radio
27}
28
29func (c Circulo) Nombre() string {
30    return fmt.Sprintf("Circulo (r=%.1f)", c.Radio)
31}
32
33// --- Rectangulo ---
34type Rectangulo struct {
35    Ancho float64
36    Alto  float64
37}
38
39func (r Rectangulo) Area() float64 {
40    return r.Ancho * r.Alto
41}
42
43func (r Rectangulo) Perimetro() float64 {
44    return 2 * (r.Ancho + r.Alto)
45}
46
47func (r Rectangulo) Nombre() string {
48    return fmt.Sprintf("Rectangulo (%.1fx%.1f)", r.Ancho, r.Alto)
49}
50
51// --- Triangulo ---
52type Triangulo struct {
53    Base   float64
54    Altura float64
55    LadoA  float64
56    LadoB  float64
57    LadoC  float64
58}
59
60func (t Triangulo) Area() float64 {
61    return (t.Base * t.Altura) / 2
62}
63
64func (t Triangulo) Perimetro() float64 {
65    return t.LadoA + t.LadoB + t.LadoC
66}
67
68func (t Triangulo) Nombre() string {
69    return fmt.Sprintf("Triangulo (b=%.1f, h=%.1f)", t.Base, t.Altura)
70}
71
72// --- Funciones que trabajan con la interface ---
73func imprimirReporte(figuras []Figura) {
74    fmt.Println("====================================")
75    fmt.Println("  REPORTE DE FIGURAS GEOMETRICAS")
76    fmt.Println("====================================")
77
78    areaTotal := 0.0
79    for _, fig := range figuras {
80        area := fig.Area()
81        areaTotal += area
82        fmt.Printf("\n> %s\n", fig.Nombre())
83        fmt.Printf("  Area:      %.2f\n", area)
84        fmt.Printf("  Perimetro: %.2f\n", fig.Perimetro())
85    }
86
87    fmt.Println("\n------------------------------------")
88    fmt.Printf("Area total combinada: %.2f\n", areaTotal)
89}
90
91func figuraMasGrande(figuras []Figura) Figura {
92    // Ordenar por area (de mayor a menor)
93    sort.Slice(figuras, func(i, j int) bool {
94        return figuras[i].Area() > figuras[j].Area()
95    })
96    return figuras[0]
97}
98
99func main() {
100    figuras := []Figura{
101        Circulo{Radio: 5},
102        Rectangulo{Ancho: 10, Alto: 4},
103        Triangulo{Base: 6, Altura: 8, LadoA: 6, LadoB: 8, LadoC: 10},
104        Circulo{Radio: 3},
105        Rectangulo{Ancho: 7, Alto: 7},
106    }
107
108    imprimirReporte(figuras)
109
110    mayor := figuraMasGrande(figuras)
111    fmt.Printf("\nFigura mas grande: %s (area: %.2f)\n", mayor.Nombre(), mayor.Area())
112}
Siguiente articulo: En la parte 7 exploraremos la concurrencia en Go con goroutines y channels. Descubriremos por que Go es tan popular para programas concurrentes y como escribir codigo que ejecuta multiples tareas al mismo tiempo de forma segura.
Share:
CV

Cristhian Villegas

Software Engineer specializing in Java, Spring Boot, Angular & AWS. Building scalable distributed systems with clean architecture.

Comments

Sign in to leave a comment

No comments yet. Be the first!

Related Articles