Semántica de puntero y valor para determinar el receptor de un método

Crear nuevos tipos de datos es una parte importante del trabajo de cada programador. En la mayoría de los idiomas, una definición de tipo consiste en una descripción de sus campos y métodos. En Golang, además de esto, debe decidir qué semántica del destinatario utilizarán los métodos del nuevo tipo: valor (valor) o puntero (puntero). A primera vista, esta decisión puede parecer secundaria, porque en la mayoría de los casos el programa funcionará con cualquier semántica del destinatario. Por lo tanto, muchas personas omiten este punto y escriben código, y no lo han descubierto hasta el final, lo que se ve afectado por la semántica del destinatario del método. Y para resolverlo, debes profundizar un poco más sobre cómo funciona Golang.

Considere un pequeño ejemplo. Defina una estructura cat con un campo Nombre y el método sayHello (cadena de persona) . De aquí en adelante, mediante un método, me referiré a una función asociada con un tipo particular, un objeto a una variable que tiene métodos, y el destinatario del método será la variable indicada entre paréntesis después de la palabra func en la descripción del método.

type cat struct { Name string } func (c *cat) sayHello(person string) { fmt.Println(fmt.Sprintf("Meow, meow, %s!", person) } 

Si definimos un puntero a cat y le solicitamos el campo Nombre , entonces, obviamente, obtendremos un error, ya que el campo se llama desde cero :

 var c *cat // c=nil fmt.Println(c.Name) //panic: runtime error: invalid memory address or nil pointer dereference 

https://play.golang.org/p/L3FnRJXKqs0

Sin embargo, cuando se llama al método sayHello () en la misma variable, no habrá error:

 var c *cat // c=nil c.sayHello(“Human”) //Meow, meow, Human! 

https://play.golang.org/p/EMoFgKL1HEi

¿Por qué nil puede llamar a un método en este ejemplo, y cómo se explica esto en términos de la arquitectura del lenguaje en sí? Esto es posible porque el método en Go es azúcar sintáctico o, en otras palabras, un contenedor alrededor de una función que tiene uno de los argumentos del destinatario. Cuando se llama al método c.sayHello ("Humano" ), en realidad se llamará a la construcción ( * cat) .sayHello (c, s) ( https://play.golang.org/p/X9leJeIvxcA ). Al llamar al método nil del ejemplo anterior, prácticamente llamamos a la función con nil en los argumentos, y esto ya es una situación bastante normal. Por lo tanto, en Go nil, es el destinatario correcto para los métodos.

Dado que el receptor del método es en realidad un argumento, las recomendaciones para usar la semántica "valor" o "puntero" para el receptor del método son similares a las recomendaciones para los argumentos de la función. A su vez, se infieren de la regla básica de Go: los argumentos siempre se pasan a la función por valor . Esto significa que la transferencia de cualquier argumento a la función ocurre a través de su copia: si la función acepta una estructura como entrada, entonces una copia completa de esta estructura entrará dentro de ella; Si toma un puntero a un objeto, entonces una nueva variable vendrá con un puntero al mismo objeto. Esto se puede ver comparando la dirección variable antes de pasarla a la función con la dirección del argumento dentro de la función ( https://play.golang.org/p/oc2ssC_Irs8 , https://play.golang.org/p/FeQa2HUdX0a ).

Cuando se usa el enlace de paso:

  • Para grandes estructuras. El puntero ocupa solo una palabra de máquina (32, 64 bits dependiendo del sistema). Por lo tanto, cuando se llama a un método con un puntero en el receptor, copiar el puntero es más barato que copiar todo el objeto, como sería el caso pasando el valor.
  • Si el método llamado modifica los datos del propio objeto. Cuando el destinatario se transfiere por referencia, el método puede afectar el estado del objeto que realiza la llamada indirectamente haciendo cambios. Lo cual es imposible al pasar por valor.

Cuando se utiliza la transferencia de valor:

  • Para tipos integrados simples como números, cadenas, bool. Cuando se usa el puntero, se usa casi la misma cantidad de memoria que el objeto de este tipo, y el costo de su mantenimiento por parte del recolector de basura aumenta, como se describirá a continuación.
  • Para sectores, así como otros tipos de referencia: mapa y canales, no tiene sentido tomar un puntero. Ellos mismos ya son un puntero.
  • Con subprocesos múltiples, pasar por valor es seguro, a diferencia de pasar por referencia.
  • Para estructuras pequeñas. En tales casos, la transmisión por valor es más eficiente. Esto se debe a que los datos internos de los métodos se colocan en un marco separado de la pila. Después de salir de una función, su marco se borra. Cuando revolvemos algo a lo largo del puntero, transferimos estos datos de la pila al montón, desde donde estos datos pueden estar disponibles para otras funciones. Aumentar el almacenamiento dinámico crea una carga adicional para el recolector de basura, cuya operación reduce la velocidad del programa en un promedio del 25%. Cuando se utiliza la transferencia de valor por valor, los datos permanecen en la pila y no se requiere trabajo adicional de recolección de basura.

Cuando necesite pensar en la semántica del destinatario:

  • El tipo de destinatario puede variar según el área temática. En uno de sus discursos, Bill Kennedy dio un buen ejemplo con el tipo de usuario que describe al usuario. Cuando se pasa por valor, se creará una copia para el usuario. Esto conducirá al hecho de que varias copias del mismo usuario pueden coexistir en el programa al mismo tiempo, lo que puede cambiarse de forma independiente, lo que no corresponde al área temática, porque el usuario real siempre es uno y no puede ser descrito en diferentes momentos por diferentes conjuntos. datos
  • Otra forma segura de determinar el tipo de destinatario para un método es usar el método constructor para su tipo. Si el constructor devuelve un valor / puntero, al crear una entidad, se supone que continuará trabajando con él como un valor / puntero. Por lo tanto, también es mejor usar la misma semántica en el receptor del método.
  • Hay una regla no escrita, en violación de la cual el compilador no jurará, pero su código definitivamente no mejorará con esto. Si uno de los métodos de tipo usa un puntero / valor como receptor, para mantener la coherencia, los métodos restantes deben usar un puntero / valor. Los métodos de tipo no deben tener un hash de receptores de valor y puntero.

Cual es el resultado


En Go Value, la semántica significa copiar un valor; la semántica del puntero significa dar acceso a un valor. Esto se aplica tanto a los argumentos de los métodos como a sus destinatarios. Para los tipos integrados, como números, líneas, sectores, mapas, canales y estructuras pequeñas, casi siempre necesita usar una transferencia basada en valores. Para las estructuras que ocupan una gran cantidad de memoria y las estructuras cuyo estado se puede cambiar indirectamente por sus métodos, debe utilizar la transferencia por referencia. Además, la semántica del destinatario puede depender del dominio que describe el tipo, la semántica devuelta en su fábrica y la semántica del destinatario ya utilizada en otros métodos de este tipo.

Source: https://habr.com/ru/post/469859/


All Articles