Skip to content

Métodos en Go

La diferencia entre métodos y funciones es que los métodos tienen un receptor, mientras que las funciones no, y solo los tipos personalizados pueden tener métodos. Veamos un ejemplo.

go
type IntSlice []int

func (i IntSlice) Get(index int) int {
  return i[index]
}
func (i IntSlice) Set(index, val int) {
  i[index] = val
}

func (i IntSlice) Len() int {
  return len(i)
}

Primero se declara un tipo IntSlice, cuyo tipo subyacente es []int, luego se declaran tres métodos Get, Set y Len. La apariencia de los métodos no es muy diferente de las funciones, solo que tienen un pequeño segmento adicional (i IntSlice). i es el receptor, IntSlice es el tipo del receptor. El receptor es similar a this o self en otros lenguajes, solo que en Go se debe especificar explícitamente.

go
func main() {
   var intSlice IntSlice
   intSlice = []int{1, 2, 3, 4, 5}
   fmt.Println(intSlice.Get(0))
   intSlice.Set(0, 2)
   fmt.Println(intSlice)
   fmt.Println(intSlice.Len())
}

El uso de métodos es similar a llamar a un método miembro de una clase, primero se declara, luego se inicializa, y finalmente se llama.

Receptor de valor

Los receptores también se dividen en dos tipos, receptor de valor y receptor de puntero. Veamos un ejemplo

go
type MyInt int

func (i MyInt) Set(val int) {
   i = MyInt(val) // se modificó, pero no causará ningún efecto
}

func main() {
   myInt := MyInt(1)
   myInt.Set(2)
   fmt.Println(myInt)
}

Después de ejecutar el código anterior, se verá que el valor de myInt sigue siendo 1, no se ha modificado a 2. Cuando se llama a un método, el valor del receptor se pasa al método. En el ejemplo anterior, el receptor es un receptor de valor, que se puede considerar simplemente como un parámetro formal, y modificar el valor de un parámetro formal no causará ningún efecto fuera del método. Entonces, ¿qué pasa si se llama a través de un puntero?

go
func main() {
  myInt := MyInt(1)
  (&myInt).Set(2)
  fmt.Println(myInt)
}

Lamentablemente, este código todavía no puede modificar el valor interno. Para coincidir con el tipo del receptor, Go lo desreferenciará, interpretándolo como (*(&myInt)).Set(2).

Receptor de puntero

Con una pequeña modificación, se puede modificar normalmente el valor de myInt.

go
type MyInt int

func (i *MyInt) Set(val int) {
   *i = MyInt(val)
}

func main() {
   myInt := MyInt(1)
   myInt.Set(2)
   fmt.Println(myInt)
}

Ahora el receptor es un receptor de puntero. Aunque myInt es un tipo de valor, cuando se llama a un método con receptor de puntero a través de un tipo de valor, Go lo interpretará como (&myint).Set(2). Por lo tanto, cuando el receptor del método es un puntero, independientemente de si el llamador es un puntero o no, se puede modificar el valor interno.

En el proceso de paso de parámetros de función, es una copia de valor. Si se pasa un entero, se copia ese entero, si es un slice, se copia ese slice, pero si es un puntero, solo se necesita copiar ese puntero. Obviamente, pasar un puntero consume menos recursos que pasar un slice. Lo mismo aplica para los receptores, receptor de valor y receptor de puntero siguen la misma lógica. En la mayoría de los casos, se recomienda usar receptor de puntero, sin embargo, no se deben mezclar, o se usan todos de un tipo o ninguno. Veamos un ejemplo.

TIP

Se necesita conocer primero interfaces

go
type Animal interface {
   Run()
}

type Dog struct {
}

func (d *Dog) Run() {
   fmt.Println("Run")
}

func main() {
   var an Animal
   an = Dog{}
   // an = &Dog{} forma correcta
   an.Run()
}

Este código no podrá compilar, el compilador dará el siguiente error

cannot use Dog{} (value of type Dog) as type Animal in assignment:
  Dog does not implement Animal (Run method has pointer receiver)

Traducido, no se puede usar Dog{} para inicializar una variable de tipo Animal, porque Dog no implementa Animal. Hay dos soluciones, una es cambiar el receptor de puntero a receptor de valor, la otra es cambiar Dog{} a &Dog{}. A continuación se explica cada una.

go
type Dog struct {
}

func (d Dog) Run() { // se cambió a receptor de valor
   fmt.Println("Run")
}

func main() { // puede ejecutarse normalmente
   var an Animal
   an = Dog{}
   // an = &Dog{} igualmente posible
   an.Run()
}

En el código original, el receptor del método Run es *Dog, naturalmente quien implementa la interfaz Animal es el puntero de Dog, no la estructura Dog, son dos tipos diferentes, por lo que el compilador considerará que Dog{} no es una implementación de Animal, y no se puede asignar a la variable an. Por lo tanto, la segunda solución es asignar un puntero de Dog a la variable an. Sin embargo, cuando se usa un receptor de valor, un puntero de Dog todavía se puede asignar normalmente a animal, esto es porque Go insertará automáticamente el operador de puntero en los casos apropiados, porque a través de un puntero se puede encontrar la estructura Dog, pero al revés, no se puede encontrar el puntero de Dog a través de la estructura Dog. Si simplemente se mezclan receptores de valor y de puntero en una estructura no hay problema, pero cuando se usan junto con interfaces, aparecerán errores. Mejor usar siempre receptores de valor o siempre receptores de puntero, para formar una buena convención y reducir la carga de mantenimiento posterior.

Hay otro caso, cuando el receptor de valor es direccionable, Go insertará automáticamente el operador de puntero para la llamada. Por ejemplo, un slice es direccionable, todavía se pueden modificar sus valores internos a través de un receptor de valor. Como el siguiente código

go
type Slice []int

func (s Slice) Set(i int, v int) {
  s[i] = v
}

func main() {
  s := make(Slice, 1)
  s.Set(0, 1)
  fmt.Println(s)
}

Salida

[1]

Pero esto causará otro problema, si se le agregan elementos, la situación es diferente. Veamos el siguiente ejemplo

type Slice []int

func (s Slice) Set(i int, v int) {
  s[i] = v
}

func (s Slice) Append(a int) {
  s = append(s, a)
}

func main() {
  s := make(Slice, 1, 2)
  s.Set(0, 1)
  s.Append(2)
  fmt.Println(s)
}
[1]

Su salida sigue siendo igual que antes. La función append tiene un valor de retorno, después de agregar elementos al slice se debe sobrescribir el slice original, especialmente después de una expansión. Modificar un receptor de valor en un método no causará ningún efecto, esto causa el resultado del ejemplo. Cambiando a receptor de puntero funciona correctamente.

go
type Slice []int

func (s *Slice) Set(i int, v int) {
  (*s)[i] = v
}

func (s *Slice) Append(a int) {
  *s = append(*s, a)
}

func main() {
  s := make(Slice, 1, 2)
  s.Set(0, 1)
  s.Append(2)
  fmt.Println(s)
}

Salida

[1 2]

Golang editado por www.golangdev.cn