Las funciones le permiten organizar la lógica en procedimientos repetibles que pueden usar diferentes argumentos cada vez que se ejecutan. Durante la definición de las funciones, a menudo observará que varias funciones pueden funcionar sobre los mismos datos en cada ocasión. Go reconoce este patrón y le permite definir funciones especiales, llamadas métodos, cuya finalidad es operar sobre instancias de algún tipo específico, conocidas como receptores. Añadir métodos a los tipos le permite comunicar no solo lo que representan los datos, sino también cómo deberían usarse.
La sintaxis para definir un método es similar a la que se usa para definir una función. La única diferencia es la adición de un parámetro después de la palabra clave func
para especificar el receptor del método. El receptor es una declaración del tipo en el que desea definir el método. El siguiente ejemplo define un método sobre un tipo estructura:
package main
import "fmt"
type Creature struct {
Name string
Greeting string
}
func (c Creature) Greet() {
fmt.Printf("%s says %s", c.Name, c.Greeting)
}
func main() {
sammy := Creature{
Name: "Sammy",
Greeting: "Hello!",
}
Creature.Greet(sammy)
}
Si ejecuta este código, el resultado será el siguiente:
OutputSammy says Hello!
Creamos un “struct” llamado Creature
con campos string
para un Name
y un Greeting
. Este Creature
tiene un único método definido, Greet
. En la declaración del receptor, asignamos la instancia de Creature
a la variable c
para poder hacer referencia a los campos de Creature
a medida que preparamos el mensaje de saludo en fmt.Printf
.
En otros lenguajes, normalmente se hace referencia al receptor de las invocaciones del método mediante una palabra clave (por ejemplo, this
o self
). Go considera que el receptor es una variable como cualquier otra, de modo que puede darle el nombre que usted prefiera. El estilo preferido por la comunidad para este parámetro es una versión en minúsculas del primer carácter del tipo receptor. En este ejemplo, usamos c
porque el tipo receptor era Creature
.
En el cuerpo de main
, creamos una instancia de Creature
y especificamos los valores para sus campos Name
y Greetings
. Invocamos el método Greet
aquí uniendo el nombre del tipo y el nombre del método con .
y proporcionando la instancia de Creature
como primer argumento.
Go ofrece otra forma más conveniente de invocar métodos en instancias de un struct, como se muestra en este ejemplo:
package main
import "fmt"
type Creature struct {
Name string
Greeting string
}
func (c Creature) Greet() {
fmt.Printf("%s says %s", c.Name, c.Greeting)
}
func main() {
sammy := Creature{
Name: "Sammy",
Greeting: "Hello!",
}
sammy.Greet()
}
Si ejecuta esto, el resultado será el mismo que en el ejemplo anterior:
OutputSammy says Hello!
Este ejemplo es idéntico al anterior, pero esta vez usamos notación de puntos para invocar el método Greet
usando el Creature
guardado en la variable sammy
como el receptor. Esta es una notación abreviada para la invocación de función del primer ejemplo. En la biblioteca estándar y la comunidad de Go se prefiere este estilo a tal extremo que en raras ocasiones verá el estilo de invocación de función previamente mostrado.
En el siguiente ejemplo, se muestra un motivo por el cual la notación de puntos es más frecuente:
package main
import "fmt"
type Creature struct {
Name string
Greeting string
}
func (c Creature) Greet() Creature {
fmt.Printf("%s says %s!\n", c.Name, c.Greeting)
return c
}
func (c Creature) SayGoodbye(name string) {
fmt.Println("Farewell", name, "!")
}
func main() {
sammy := Creature{
Name: "Sammy",
Greeting: "Hello!",
}
sammy.Greet().SayGoodbye("gophers")
Creature.SayGoodbye(Creature.Greet(sammy), "gophers")
}
Si ejecuta este código, el resultado tiene este aspecto:
OutputSammy says Hello!!
Farewell gophers !
Sammy says Hello!!
Farewell gophers !
Modificamos los ejemplos anteriores para introducir otro método llamado SayGoodbye
y también cambiamos Greet
para que muestre Creature
, de modo que podamos invocar métodos adicionales en esa instancia. En el cuerpo de main
, invocamos los métodos Greet
y SayGoodbye
en la variable sammy
primero usando la notación de puntos y luego usando el estilo de invocación funcional.
Los resultados de ambos estilos son los mismos, pero el ejemplo en el que se utiliza la notación de punto es mucho más legible. La cadena de puntos también nos indica la secuencia en la cual se invocarán los métodos, mientras que el estilo funcional invierte esta secuencia. La adición de un parámetro a la invocación SayGoodbye
oculta más el orden de las invocaciones del método. La claridad de la notación de puntos es el motivo por el cual es el estilo preferido para invocar métodos en Go, tanto en la biblioteca estándar como entre los paquetes externos que encontrará en el ecosistema de Go.
La definición de métodos en tipos, en contraposición la definición de funciones que operan en algún valor, tiene otra relevancia especial en el lenguaje de programación Go. Los métodos son el concepto principal que subyace a las interfaces.
Cuando define un método en cualquier tipo en Go, ese método se añade al conjunto de métodos del tipo. El conjunto de métodos es el grupo de funciones asociadas con ese tipo como métodos y el compilador de Go los utiliza para definir si algún tipo puede asignarse a una variable con un tipo de interfaz. Un tipo de interfaz es una especificación de métodos usados por el compilador para garantizar que un tipo proporcione implementaciones para esos métodos. Se dice que cualquier tipo que tenga métodos con el mismo nombre, los mismos parámetros y los mismos valores de retorno que los que se encuentran en la definición de una interfaz_ implementan _esa interfaz y pueden asignarse a variables con ese tipo de interfaz. La siguiente es la definición de la interfaz fmt.Stringer
de la biblioteca estándar:
type Stringer interface {
String() string
}
Para que un tipo implemente la interfaz fmt.Stringer
, debe proporcionar un método String()
que muestre una string
. Implementar esta interfaz permitirá imprimir su tipo exactamente como lo desee (a veces esto se denomina “pretty-printed”) cuando pasa las instancias de su tipo a las funciones definidas en el paquete fmt
. En el siguiente ejemplo, se define un tipo que implementa esta interfaz:
package main
import (
"fmt"
"strings"
)
type Ocean struct {
Creatures []string
}
func (o Ocean) String() string {
return strings.Join(o.Creatures, ", ")
}
func log(header string, s fmt.Stringer) {
fmt.Println(header, ":", s)
}
func main() {
o := Ocean{
Creatures: []string{
"sea urchin",
"lobster",
"shark",
},
}
log("ocean contains", o)
}
Cuando ejecute el código, verá este resultado:
Outputocean contains : sea urchin, lobster, shark
En este ejemplo se define un nuevo tipo de struct llamado Ocean
. Se dice que Ocean
implementa la interfaz fmt-Stringer
porque Ocean
define un método llamado String
, que no toma ningún parámetro y muestra una string
. En main
, definimos un nuevo Ocean
y lo pasamos a una función log
, que toma una string
para imprimir primero, seguida de cualquier elemento que implemente fmt.Stringer
. El compilador de Go nos permite pasar o
aquí porque Ocean
implementa todos los métodos solicitados por fmt.Stringer
. En log
usamos fmt.PrintIn
, que invoca el método String
de Ocean
cuando encuentra un fmt.Stringer com
o uno de sus parámetros.
Si Ocean
no proporcionara un método String()
, Go produciría un error de compilación, porque el método log
solicita un fmt.Stringer
como su argumento. El error tiene este aspecto:
Outputsrc/e4/main.go:24:6: cannot use o (type Ocean) as type fmt.Stringer in argument to log:
Ocean does not implement fmt.Stringer (missing String method)
Go también garantizará que el método String()
proporcionado coincida exactamente con el solicitado por la interfaz fmt.Stringer
. Si no es así, producirá un error similar a este:
Outputsrc/e4/main.go:26:6: cannot use o (type Ocean) as type fmt.Stringer in argument to log:
Ocean does not implement fmt.Stringer (wrong type for String method)
have String()
want String() string
En los ejemplos analizados hasta el momento, definimos métodos en el receptor del valor. Es decir, si usamos la invocación funcional de métodos, el primer parámetro, que se refiere al tipo en el cual el método se definió, será un valor de ese tipo en vez de un puntero. Por lo tanto, cualquier modificación que realicemos a la instancia proporcionada al método se descartará cuando el método complete la ejecución, ya que el valor recibido es una copia de los datos. También es posible definir métodos sobre el receptor de punteros de un tipo.
La sintaxis para definir métodos en el receptor de punteros es casi idéntica a los métodos de definición en el receptor de valores. La diferencia radica en crear un prefijo en el nombre del tipo de la declaración del receptor con un asterisco (*
). En el siguiente ejemplo, se define un método sobre el receptor de punteros para un tipo:
package main
import "fmt"
type Boat struct {
Name string
occupants []string
}
func (b *Boat) AddOccupant(name string) *Boat {
b.occupants = append(b.occupants, name)
return b
}
func (b Boat) Manifest() {
fmt.Println("The", b.Name, "has the following occupants:")
for _, n := range b.occupants {
fmt.Println("\t", n)
}
}
func main() {
b := &Boat{
Name: "S.S. DigitalOcean",
}
b.AddOccupant("Sammy the Shark")
b.AddOccupant("Larry the Lobster")
b.Manifest()
}
Verá el siguiente resultado cuando ejecute este ejemplo:
OutputThe S.S. DigitalOcean has the following occupants:
Sammy the Shark
Larry the Lobster
En este ejemplo, se definió un tipo Boat
con un Name
y occupants
. Queremos introducir de forma forzosa código en otros paquetes para añadir únicamente ocupantes con el método AddOccupant
; por lo tanto, hicimos que el campo occupants
no se exporte poniendo en minúsculas la primera letra del nombre del campo. También queremos controlar que la invocación de AddOccupant
haga que la instancia de Boat
se modifique, por eso definimos AddOccupant
en el receptor de punteros. Los punteros actúan más como una referencia a una instancia específica de un tipo que como una copia de ese tipo. Saber que AddOccupant
se invocará usando un puntero a Boat
garantiza que cualquier modificación persista.
En main
, definimos una nueva variable, b
, que tendrá un puntero a Boat
(*Boat
). Invocamos el método AddOccupant
dos veces en esta instancia para añadir dos pasajeros. El método Manifest
se define en el valor Boat
, porque en su definición, el receptor se especifica como (b Boat)
. En main
, aún podemos invocar Manifest
porque Go puede eliminar la referencia del puntero de forma automática para obtener el valor Boat
. b.Manifest()
aquí es equivalente a (*b). Manifest()
.
Si un método se define en un receptor de punteros o en un receptor de valor tiene implicaciones importantes cuando se intenta asignar valores a variables que son tipos de interfaz.
Cuando se asigne un valor a una variable con un tipo de interfaz, el compilador de Go examinará el conjunto de métodos del tipo que se asigna para garantizar que tenga los métodos previstos por la interfaz. Los conjuntos de métodos para el receptor de punteros y el receptor de valores son diferentes porque los métodos que reciben un puntero pueden modificar sus receptores, mientras que aquellos que reciben un valor no pueden hacerlo.
En el siguiente ejemplo, se demuestra la definición de dos métodos: uno en el receptor de punteros de un tipo y otro en su receptor de valores. Sin embargo, solo el receptor de punteros podrá satisfacer los requisitos de la interfaz también definida en este ejemplo:
package main
import "fmt"
type Submersible interface {
Dive()
}
type Shark struct {
Name string
isUnderwater bool
}
func (s Shark) String() string {
if s.isUnderwater {
return fmt.Sprintf("%s is underwater", s.Name)
}
return fmt.Sprintf("%s is on the surface", s.Name)
}
func (s *Shark) Dive() {
s.isUnderwater = true
}
func submerge(s Submersible) {
s.Dive()
}
func main() {
s := &Shark{
Name: "Sammy",
}
fmt.Println(s)
submerge(s)
fmt.Println(s)
}
Cuando ejecute el código, verá este resultado:
OutputSammy is on the surface
Sammy is underwater
En este ejemplo, se definió una interfaz llamada Submersible
que prevé tipos que tengan un método Dive()
. A continuación definimos un tipo Shark
con un campo Name
y un método isUnderwater
para realizar un seguimiento del estado de Shark
. Definimos un método Dive()
en el receptor de punteros de Shark
que cambió el valor de isUnderwater
a true
. También definimos el método String()
del receptor de valores para que pudiera imprimir correctamente el estado de Shark
con fmt.PrintIn
usando la interfaz fmt.Stringer
aceptada por fmt.PrintIn
que observamos antes. También usamos una función submerge
que toma un parámetro Submersible
.
Usar la interfaz Submersible
en vez de *Shark
permite que la función submerge
dependa solo del comportamiento proporcionado por un tipo. Esto hace que la función submerge
sea más reutilizable porque no tendrá que escribir nuevas funciones submerge
para Submarine
, Whale
o cualquier otro elemento de tipo acuático que aún no se nos haya ocurrido. Siempre que definamos un método Dive()
, puede usarse con la función submerge
.
En main
, definimos una variable s
que es un puntero de un Shark
y se imprime inmediatamente s
con fmt.PrintIn
. Esto muestra la primera parte del resultado, Sammy is on the surface
. Pasamos s
a submerge
y luego invocamos fmt.PrintIn
de nuevo con s
como argumento para ver la segunda parte del resultado impresa: Sammy is underwater
.
Si cambiamos s
para que sea Shark
en vez de *Shark,
el compilador de Go generará un error:
Outputcannot use s (type Shark) as type Submersible in argument to submerge:
Shark does not implement Submersible (Dive method has pointer receiver)
El compilador de Go indica que Shark
no tiene un método Dive
, solo se define en el receptor de punteros. Cuando ve este mensaje en su propio código, la solución es pasar un puntero al tipo de interfaz usando el operador &
antes de la variable en la que se asignó el tipo de valor.
Declarar métodos en Go no difiere de definir funciones que reciben diferentes tipos de variables. Se aplican las mismas reglas que para trabajar con punteros. Go proporciona algunas utilidades para esta definición de funciones extremadamente común y las recoge en conjuntos de métodos que pueden probarse a través de tipos de interfaz. Usar los métodos de forma efectiva le permitirá trabajar con interfaces en su código para mejorar su capacidad de prueba y proporciona una mejor organización para los lectores futuros de su código.
Si desea obtener más información acerca del lenguaje de programación Go en general, consulte nuestra serie Cómo escribir código en Go.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!