Anatomía de los métodos en Go

Como no tenemos clases en Go, puedes decir que struct hará un trabajo para hacer objetos. Pero en OOP, las clases tienen propiedades ( campos ) y comportamientos ( métodos ) y hasta ahora solo hemos visto propiedades de una estructura.

Para todos aquellos programadores que no son OOP, el comportamiento es una acción que un objeto puede realizar. Por ejemplo, el Dog es un tipo de Animal y el Dog puede bark . Por lo tanto, ladrar es un comportamiento de la clase animal Dog . Por lo tanto, cualquier objeto (instancia) de la clase Dog tendrá este comportamiento.

Hemos visto en la lección de estructuras, especialmente en la sección del campo de función que, el campo de estructura también puede ser una función . ¡Podemos agregar un campo de bark de tipo de función que no toma argumentos y devuelve una cadena woof woof! . Pero esto no se adhiere al concepto de OOP ya que los campos de struct no tienen ninguna idea de la struct que pertenecen . Por lo tanto, los métodos vienen al rescate.

☛ ¿Qué es un método?

Como sabe que el campo de estructura también puede ser una función, el concepto de método será muy fácil de entender. Un método no es más que una función, pero pertenece a un tipo determinado. Un método se define con una sintaxis diferente a la función normal. Se requiere un parámetro adicional conocido como receptor, que es un tipo al que pertenece el método. Un método puede acceder a las propiedades del receptor al que pertenece.

Veamos un programa simple para obtener el nombre completo de un Employee usando una función simple.

En el programa anterior, hemos creado un tipo de estructura simple Employee que tiene dos campos de cadena FirstName y LastName . Luego definimos la función fullName que toma dos cadenas y devuelve una cadena. fullname función fullname devuelve el nombre completo de un empleado concatenando estas dos cadenas. Luego creamos una estructura e fullName función fullName para obtener el nombre completo del empleado e .

Pero lo triste es que cada vez que necesitamos obtener el nombre completo de un empleado, necesitamos pasar manualmente la función FirstName y LastName a nombre fullName .

El método resuelve este problema fácilmente. Mientras hablábamos, solo necesitamos un parámetro receptor adicional en la definición de la función.

La sintaxis para definir un método es

  func (r Type) functionName (... Type) Type { 
...
}

De la sintaxis anterior, podemos decir que el método y la función tienen la misma sintaxis, excepto por una declaración de argumento de entrada (r Type) antes del nombre de la función. Type es cualquier tipo legal en Go y los argumentos de función y el valor de retorno son opcionales.

fullName método fullName usando la sintaxis anterior.

En el programa anterior, hemos definido el método fullName que no toma ningún argumento pero devuelve una cadena. Pertenece a un tipo de estructura Employee por lo tanto, utilizamos el argumento e que actuará como receptor de la estructura Employee .

El receptor en el método es accesible dentro del cuerpo del método. Por lo tanto, podemos acceder a e dentro del cuerpo del método. Usando eso, podemos acceder a cualquier campo de estructura. En el programa anterior, estamos concatenando FirstName y LastName y regresando como el nombre completo del empleado.

Como un método pertenece a un tipo de receptor, podemos llamar a ese método usando la Type.methodName(...) . En el programa anterior, hemos usado e.fullName() para obtener el nombre completo de un empleado ya que el método fullName pertenece a e .

Esto no es diferente de lo que aprendimos en la lección de structs donde la función fullName era un campo de estructura. Pero en el caso de los métodos, no tenemos que proporcionar propiedades de estructura porque el método ya los conoce.

☛ Mismo nombre de método

Una diferencia importante entre función y método es que muchos métodos pueden tener el mismo nombre, mientras que no se pueden definir dos funciones con el mismo nombre en un paquete.

Creemos dos tipos de estructura Circle y Rectangle y creemos dos métodos con el mismo nombre Area que calcula el área de su receptor.

En el programa anterior, hemos creado los tipos de estructura Rect y Circle y Rect creado dos métodos del mismo nombre Area con el tipo de receptor Rect y Circle . Se nos permite usar los mismos nombres de métodos siempre que los receptores de métodos sean de diferentes tipos. Cuando llamamos al método Area en Rect y Circle , sus respectivos métodos se ejecutan.

☛ Receptores de puntero

Hasta ahora hemos visto métodos que reciben el valor del receptor, lo que significa que los métodos solo reciben una copia del tipo al que pertenecen, por ejemplo, una estructura en ejemplos anteriores.

Para verificar eso, podemos crear un método que cambie el valor de un campo de estructura. changeName un nombre de método changeName que cambie el nombre de la estructura del empleado.

En el programa anterior, hemos llamado al método changeName en la estructura del tipo Employee mientras que en el método, estamos asignando un nuevo valor al campo de nombre.

Del resultado anterior, parece que el método changeName no hizo nada con la propiedad de name de e . Esto prueba que el receptor e en la definición del método era solo una copia de la estructura real ( del método main ), por lo tanto, cualquier cambio realizado no afectó a la estructura original.

Pero un método también puede aceptar el valor del pointer del receptor. La sintaxis para definir un método con receptor de pointer es muy similar al método normal. En la siguiente definición, le indicamos a Go que este método recibirá un receptor de puntero.

  func (r * Type) methodName (... Type) Type { 
...
}

Reescribamos el ejemplo anterior con un método que recibe el receptor del puntero.

Veamos qué cambios hicimos.

  • Cambiamos la definición del método para recibir un receptor de puntero usando * . Ahora el método changeName recibirá el puntero del receptor, eso significa el valor original. Dentro del cuerpo del método, estamos convirtiendo el puntero del receptor al valor del receptor usando * , por lo tanto (*e) será el valor real almacenado en la memoria. Por lo tanto, cualquier cambio realizado en él se reflejará en el valor original de la estructura del receptor.
  • Luego creamos un puntero ep que apunta a la estructura e .
  • Al llamar changeName método changeName en struct e , debemos llamarlo en el puntero del mismo, que será ep . Dado que el método pertenece al puntero de e lugar del valor de e . Esto pasará el puntero ep al método en lugar del valor e .

En el programa anterior, creamos el puntero ep para llamar al método, pero puede usar la sintaxis (&e).changeName("Monica Geller") lugar de crear un nuevo puntero.

Todo esto suena poco complejo. Pero no se preocupe, Go lo simplifica al proporcionar algunos accesos directos. Reescribamos la programación anterior usando los atajos de Go.

El programa anterior funcionará bien como antes. Entonces, qué cambió.

  • Si un método recibe un receptor de puntero, no necesita usar la sintaxis (*e) para deferenciar el puntero u obtener el valor del puntero. Puede usar e simple, que será la dirección del valor al que apunta el puntero, pero Go comprenderá que está intentando realizar una operación en el valor mismo y debajo del capó, hará que e (*e) .
  • Además, no necesita llamar a un método en el puntero si el método recibe un receptor de puntero. En su lugar, puede usar el valor tal como está, e llamar a un método en él. Go pasará el puntero de e debajo del capó si el método espera un receptor de puntero.

Puede decidir entre un método con receptor de puntero o receptor de valor según su caso de uso. Pero, en general, incluso si no desea cambiar los datos del receptor, se utilizan métodos con receptor de puntero ya que no se crea una nueva memoria para las operaciones. Lo que sucede al pasar una copia del receptor, lo que ocurre en el caso de métodos con valor receptor.

☛ Métodos en estructura anidada

Como un campo de estructura también puede ser una estructura, podemos definir un método en la estructura principal y acceder a la estructura anidada para hacer lo que queramos.

Si la estructura interna implementa un método, puede llamar a un método utilizando . ( punto ) de acceso.

Pero, ¿qué sucederá si el campo anidado es anónimo? Recuerde que los campos anónimos donde un campo no tiene nombre o el nombre del campo se deriva de su tipo. En caso de que el campo sea de un tipo de estructura, se promoverán los campos de estructura anidados.

Veamos qué sucederá con los métodos en un caso cuando el campo de estructura es anónimo.

Como vimos en la lección de las structs si el campo anidado es una estructura anónima, sus campos serán accesibles desde la estructura principal. Por lo tanto, cualquier método que acepte el receptor de estructura también tendrá acceso a los campos promocionados.

☛ Métodos promocionados

Al igual que los campos promovidos en una estructura, los métodos implementados por la estructura interna están disponibles en la estructura principal. Como vimos en el ejemplo anterior, el campo Contact es anónimo. Por lo tanto, podríamos acceder a e.phone , el número de teléfono de la estructura interna a medida que phone campo del phone se promociona a la estructura del Employee . En el mismo escenario, cualquier método implementado por Contact struct estará disponible en Employee struct. Reescribamos el ejemplo anterior.

Solo hicimos un cambio para changePhone función del changePhone . En lugar de recibir el tipo de Employee , el método ahora espera el receptor del puntero de Contact . Dado que se promueve el campo Contact , también se promocionará cualquier método en él. Por lo tanto, podríamos usar e.changePhone como si el tipo Employee de estructura e implementara el método changePhone .

☛ Un método puede aceptar tanto el puntero como el valor

Cuando una función tiene un argumento de valor, solo aceptará el valor del parámetro. Si pasó un puntero a la función que espera un valor, no funcionará. Esto también es cierto cuando la función acepta el puntero, simplemente no puede pasarle un valor.

Cuando se trata de un método, ese no es el caso. Podemos definir un método con valor o recibir puntero y llamarlo en puntero o valor.

En el programa anterior, changeName método changeName que acepta un puntero pero recurrimos al valor e que es legal porque Go under the hood le pasará un puntero de e . Además, showSalary método showSalary que acepta el valor, pero llamamos al puntero showSalary que es legal porque Go under the hood le pasará el valor del puntero.

Intentamos cambiar el salario de e dentro del método showSalary pero no funcionó como podemos ver en el resultado. Esto se debe a que incluso llamamos a este método en un puntero, Go solo envió una copia del valor a ese método.

☛ Métodos en tipo non-struct

Hasta ahora hemos visto métodos sobre el tipo de estructura, pero a partir de la definición del método, puede aceptar cualquier tipo como receptor siempre que la definición de tipo y la definición de método estén en el mismo paquete. Hasta ahora, definimos estructura y método en el mismo paquete main , por lo tanto funcionó.

Para mostrar esto, intentaremos agregar un método a toUpperCase en la string tipo incorporada.

A partir del programa anterior, creamos el método toUpperCase que acepta string como tipo de receptor. Por lo tanto, esperamos que string.toUpperCase() funcione y devuelva la versión en mayúsculas del receptor. Utilizamos el paquete de strings para convertir una cadena a mayúsculas.

Pero el programa anterior se encontrará con un error de compilación

  program.go: 8: no se pueden definir nuevos métodos en una cadena de tipo no local 
program.go: 14: str.toUpperCase undefined (el tipo string no tiene campo ni método toUpperCase)

Esto se debe a que el tipo de string y el método toUpperCase no están definidos en el mismo paquete. MyString un alias de tipo MyString de string y hagamos que funcione.

A partir del programa anterior, creamos el método toUpperCase que ahora acepta el tipo MyString . Necesitábamos modificar los elementos internos de este método para pasar el tipo de cadena a strings.ToUpper Función strings.ToUpper , pero lo conseguimos.

Ahora podemos llamar a str.toUpperCase() porque str es de tipo MyString ya que usamos conversión de tipo en la línea no. 16 para convertir del tipo de string tipo de MyString .