Structs, Metodos e Interfaces en Go: Guia Completa

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
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}
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:
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}
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.
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}
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
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}
*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.
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}
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.
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}
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
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
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).
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}
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.
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}
Comments
Sign in to leave a comment
No comments yet. Be the first!