Lección 4 de 8. Aquí está la parte de Go que más desconcierta a quien viene de Java o de C#: no hay clases, no hay herencia y las interfaces no se declaran. Lo que hay es más simple, y una vez entendido cuesta echar de menos lo otro.
Structs: datos con nombre
Un struct es un conjunto de campos. Nada más: no tiene métodos dentro ni constructor.
type Producto struct {
Nombre string
Precio float64
Unidades int
}
Se puede crear de varias formas, y una de ellas es no crearlo: como toda variable en Go, un struct declarado sin valor nace con cada campo en su valor cero.
var vacio Producto
fmt.Printf("%+v\n", vacio)
p := Producto{Nombre: "Camiseta", Precio: 19.90, Unidades: 3}
fmt.Printf("%+v\n", p)
salida{Nombre: Precio:0 Unidades:0}
{Nombre:Camiseta Precio:19.9 Unidades:3}
El verbo %+v de Printf imprime los campos con su nombre, y es la forma más rápida de ver qué contiene algo mientras depuras.
Producto{"Camiseta", 19.90, 3}, por posición, pero no lo hagas: el día que alguien añada un campo en medio, todo el código que lo use así se romperá en silencio o dejará de compilar. Con los nombres puestos, da igual el orden.Métodos: funciones con receptor
Un método es una función normal a la que se le pone delante quién la recibe. No va dentro del struct, va al lado.
func (p Producto) Total() float64 {
return p.Precio * float64(p.Unidades)
}
func (p *Producto) Rebajar(porcentaje float64) {
p.Precio = p.Precio * (1 - porcentaje/100)
}
salidatotal: 59.70 tras rebajar un 10%: 17.91
Fíjate en la diferencia entre los dos receptores: (p Producto) y (p *Producto). El primero recibe una copia; el segundo, el original.
Y por eso esto no funciona
type Contador struct{ N int }
func (c Contador) SumarMal() { c.N++ } // toca una copia
func (c *Contador) SumarBien() { c.N++ } // toca el original
c := Contador{}
c.SumarMal()
fmt.Println("tras SumarMal: ", c.N)
c.SumarBien()
fmt.Println("tras SumarBien:", c.N)
salidatras SumarMal: 0 tras SumarBien: 1
Compila sin una queja y no hace nada. Es uno de los errores silenciosos más frecuentes al empezar.
Composición en vez de herencia
Go no tiene herencia. Tiene incrustación: meter un tipo dentro de otro sin ponerle nombre de campo.
type Caja struct {
Producto // sin nombre: va incrustado
Frágil bool
}
c := Caja{Producto: Producto{Nombre: "Camiseta", Precio: 19.9}, Frágil: true}
fmt.Println(c.Nombre) // campo del Producto, como si fuera de Caja
fmt.Println(c.Etiqueta()) // y también sus métodos
fmt.Println(c.Producto.Nombre) // el camino largo, por si hay ambigüedad
salidaCamiseta Camiseta Camiseta
Parece herencia y no lo es. Caja no «es un» Producto: lo tiene, y Go se limita a dejarte escribir c.Nombre en lugar de c.Producto.Nombre. La diferencia se nota en cuanto hay dos incrustados con el mismo campo: ahí el atajo desaparece y hay que decir cuál.
Interfaces: lo que de verdad cambia
Una interfaz en Go es una lista de métodos. Y lo importante no es eso, sino lo que no hay que hacer: ningún tipo declara que la cumple.
type Describible interface {
Describir() string
}
type Producto struct{ Nombre string; Precio float64 }
func (p Producto) Describir() string { return fmt.Sprintf("%s por %.2f €", p.Nombre, p.Precio) }
type Usuario struct{ Alias string }
func (u Usuario) Describir() string { return "@" + u.Alias }
Producto y Usuario no tienen nada que ver, ni mencionan la interfaz. Pero los dos encajan, así que los dos sirven donde se pida un Describible:
func listar(cosas []Describible) string { … }
fmt.Println(listar([]Describible{
Producto{Nombre: "Camiseta", Precio: 19.90},
Usuario{Alias: "ana"},
}))
salidaCamiseta por 19.90 € · @ana
Eso tiene una consecuencia que vale la pena entender bien: puedes cumplir una interfaz que escribió otro sin tocar tu código. Si una biblioteca pide algo con un método Read y tu tipo ya lo tiene, encaja. No hay que importar nada ni modificar la declaración.
La que más vas a usar sin darte cuenta
fmt.Stringer es una interfaz de la biblioteca estándar con un solo método, String() string. Si tu tipo lo tiene, fmt lo usa automáticamente al imprimirlo.
type Moneda float64
func (m Moneda) String() string { return fmt.Sprintf("%.2f €", float64(m)) }
precio := Moneda(12.5)
fmt.Println(precio)
fmt.Printf("%v y %s\n", precio, precio)
salida12.50 € 12.50 € y 12.50 €
Nadie le dijo a fmt que usara ese método. Simplemente comprueba si lo tienes.
Preguntar en tiempo de ejecución
Con una aserción de tipo se puede comprobar si algo cumple una interfaz:
var x any = Producto{Nombre: "Gorra", Precio: 9}
if d, ok := x.(Describible); ok {
fmt.Println("sí sabe describirse:", d.Describir())
}
if _, ok := x.(fmt.Stringer); !ok {
fmt.Println("pero no es un fmt.Stringer")
}
salidasí sabe describirse: Gorra por 9.00 € pero no es un fmt.Stringer
Ese any es el nombre moderno de interface{}: la interfaz sin métodos, que por tanto cumple todo. Y reaparece el patrón valor, ok que ya vimos con los maps.
Lo que hay que llevarse
| En otros lenguajes | En Go |
|---|---|
| Clase con datos y métodos dentro | struct con los datos, y métodos al lado |
| Constructor | Un literal, o una función NuevoX() por convención |
| Herencia | Incrustación: lo tiene, no lo es |
class X implements Y |
Nada. Si tiene los métodos, cumple |
| Interfaces grandes | Interfaces de uno o dos métodos |
Todo el código de esta lección se compiló y ejecutó con Go 1.25 antes de publicarla; las salidas están copiadas de esa ejecución, incluido el ejemplo del receptor por valor que no modifica nada.