Punteros vs. valores en parámetros y valores de retorno


329

En Go hay varias formas de devolver un structvalor o una porción del mismo. Para los individuales que he visto:

type MyStruct struct {
    Val int
}

func myfunc() MyStruct {
    return MyStruct{Val: 1}
}

func myfunc() *MyStruct {
    return &MyStruct{}
}

func myfunc(s *MyStruct) {
    s.Val = 1
}

Entiendo las diferencias entre estos. El primero devuelve una copia de la estructura, el segundo un puntero al valor de estructura creado dentro de la función, el tercero espera que se pase una estructura existente y anula el valor.

He visto que todos estos patrones se usan en varios contextos, me pregunto cuáles son las mejores prácticas con respecto a estos. ¿Cuándo usarías cuál? Por ejemplo, el primero podría estar bien para estructuras pequeñas (porque la sobrecarga es mínima), el segundo para las más grandes. Y el tercero si desea ser extremadamente eficiente en la memoria, porque puede reutilizar fácilmente una sola instancia de estructura entre llamadas. ¿Hay alguna práctica recomendada para cuándo usar cuál?

Del mismo modo, la misma pregunta con respecto a las rebanadas:

func myfunc() []MyStruct {
    return []MyStruct{ MyStruct{Val: 1} }
}

func myfunc() []*MyStruct {
    return []MyStruct{ &MyStruct{Val: 1} }
}

func myfunc(s *[]MyStruct) {
    *s = []MyStruct{ MyStruct{Val: 1} }
}

func myfunc(s *[]*MyStruct) {
    *s = []MyStruct{ &MyStruct{Val: 1} }
}

De nuevo: ¿cuáles son las mejores prácticas aquí? Sé que los sectores siempre son punteros, por lo que devolver un puntero a un sector no es útil. Sin embargo, ¿debería devolver una porción de valores de estructura, una porción de punteros a estructuras, debería pasar un puntero a una porción como argumento (un patrón utilizado en la API de Go App Engine )?


1
Como dices, realmente depende del caso de uso. Todos son válidos según la situación. ¿Es este un objeto mutable? ¿Queremos una copia o puntero? etc. Por cierto, no mencionaste el uso new(MyStruct):) Pero en realidad no hay diferencia entre los diferentes métodos para asignar punteros y devolverlos.
Not_a_Golfer

15
Eso es literalmente sobre ingeniería. Las estructuras deben ser bastante grandes para que devolver un puntero haga que su programa sea más rápido. Simplemente no te molestes, codifica, perfila, arregla si es útil.
Volker

1
Solo hay una forma de devolver un valor o un puntero, y es devolver un valor o un puntero. Cómo los asigna es un tema aparte. Use lo que funcione para su situación y escriba un código antes de preocuparse por ello.
JimB

3
Por cierto, por curiosidad, me equivoqué. La devolución de estructuras frente a punteros parece ser aproximadamente la misma velocidad, pero pasar punteros a funciones por las líneas es significativamente más rápido. Aunque no está en un nivel, sería importante
Not_a_Golfer

1
@Not_a_Golfer: Supongo que solo se realiza la asignación de bc fuera de la función. Además, los valores de referencia frente a los punteros dependen del tamaño de la estructura y los patrones de acceso a la memoria después del hecho. Copiar cosas del tamaño de una línea de caché es lo más rápido que puede obtener, y la velocidad de desreferenciar punteros del caché de la CPU es muy diferente de desreferenciarlos de la memoria principal.
JimB

Respuestas:


392

tl; dr :

  • Los métodos que utilizan punteros receptores son comunes; La regla general para los receptores es : "En caso de duda, utilice un puntero".
  • Los segmentos, mapas, canales, cadenas, valores de función y valores de interfaz se implementan con punteros internamente, y un puntero hacia ellos a menudo es redundante.
  • En otros lugares, use punteros para grandes estructuras o estructuras que tendrá que cambiar y, de lo contrario, pase valores , porque hacer que las cosas cambien por sorpresa a través de un puntero es confuso.

Un caso en el que a menudo debe usar un puntero:

  • Los receptores son punteros con más frecuencia que otros argumentos. No es inusual que los métodos modifiquen la cosa en la que están llamados, o que los tipos con nombre sean estructuras grandes, por lo que la guía es predeterminar los punteros, excepto en casos excepcionales.
    • La herramienta de copyfighter de Jeff Hodges busca automáticamente receptores no pequeños pasados ​​por valor.

Algunas situaciones en las que no necesita punteros:

  • Las pautas de revisión de código sugieren pasar estructuras pequeñas como type Point struct { latitude, longitude float64 }, y tal vez incluso cosas un poco más grandes, como valores, a menos que la función que está llamando deba poder modificarlas en su lugar.

    • La semántica de valores evita situaciones de alias en las que una asignación aquí cambia un valor allá por sorpresa.
    • No es bueno sacrificar una semántica limpia por un poco de velocidad, y a veces pasar pequeñas estructuras por valor es en realidad más eficiente, porque evita errores de caché o asignaciones de almacenamiento dinámico.
    • Por lo tanto, la página de comentarios de revisión de código de Go Wiki sugiere pasar por valor cuando las estructuras son pequeñas y es probable que sigan así.
    • Si el límite "grande" parece vago, lo es; Podría decirse que muchas estructuras están en un rango donde un puntero o un valor está bien. Como límite inferior, los comentarios de revisión de código sugieren que los cortes (tres palabras de máquina) son razonables para usar como receptores de valor. Como algo más cercano a un límite superior, bytes.Replacetoma 10 palabras de args (tres rebanadas y una int).
  • Para sectores , no necesita pasar un puntero para cambiar elementos de la matriz. io.Reader.Read(p []byte)cambia los bytes de p, por ejemplo. Podría decirse que es un caso especial de "tratar pequeñas estructuras como valores", ya que internamente está pasando una pequeña estructura llamada encabezado de corte (consulte la explicación de Russ Cox (rsc) ). Del mismo modo, no necesita un puntero para modificar un mapa o comunicarse en un canal .

  • Para los cortes que volverá a cortar (cambiar el inicio / longitud / capacidad de), las funciones integradas como appendaceptar un valor de corte y devolver uno nuevo. Imitaría eso; evita el alias, devolver un nuevo segmento ayuda a llamar la atención sobre el hecho de que se puede asignar una nueva matriz, y es familiar para las personas que llaman.

    • No siempre es práctico seguir ese patrón. Algunas herramientas como las interfaces de base de datos o los serializadores deben agregarse a un segmento cuyo tipo no se conoce en el momento de la compilación. A veces aceptan un puntero a un segmento en un interface{}parámetro.
  • Los mapas, canales, cadenas y valores de función e interfaz , como los segmentos, son referencias internas o estructuras que ya contienen referencias, por lo que si solo está tratando de evitar que se copien los datos subyacentes, no necesita pasarles punteros. . (RSC escribió una publicación separada sobre cómo se almacenan los valores de la interfaz ).

    • Es posible que aún necesite pasar punteros en el caso más raro de que desee modificar la estructura de la persona que llama: flag.StringVartoma una *stringpor esa razón, por ejemplo.

Donde usa punteros:

  • Considere si su función debe ser un método en cualquier estructura para la que necesite un puntero. La gente espera xque se modifiquen muchos métodos x, por lo que hacer que la estructura modificada sea el receptor puede ayudar a minimizar la sorpresa. Hay pautas sobre cuándo los receptores deben ser punteros.

  • Las funciones que tienen efectos en sus parámetros no receptores deberían dejarlo claro en el godoc, o mejor aún, el godoc y el nombre (como reader.WriteTo(writer)).

  • Usted menciona aceptar un puntero para evitar asignaciones permitiendo la reutilización; cambiar las API por la reutilización de la memoria es una optimización que retrasaría hasta que quede claro que las asignaciones tienen un costo no trivial, y luego buscaría una forma que no fuerce la API más complicada para todos los usuarios:

    1. Para evitar asignaciones, el análisis de escape de Go es tu amigo. A veces puede ayudarlo a evitar las asignaciones de montón haciendo tipos que se pueden inicializar con un constructor trivial, un literal simple o un valor cero útil como bytes.Buffer.
    2. Considere un Reset()método para volver a poner un objeto en blanco, como ofrecen algunos tipos de stdlib. Los usuarios a quienes no les importa o no pueden guardar una asignación no tienen que llamarla.
    3. Considere la posibilidad de escribir métodos de modificación en el lugar y crear desde cero como pares coincidentes, por conveniencia: existingUser.LoadFromJSON(json []byte) errorpodría envolverse NewUserFromJSON(json []byte) (*User, error). Nuevamente, empuja la elección entre pereza y asignaciones pellizcadas a la persona que llama.
    4. Las personas que llaman que buscan reciclar memoria pueden dejar que sync.Poolmanejen algunos detalles. Si una asignación en particular crea mucha presión de memoria, está seguro de saber cuándo la asignación ya no se usa, y no tiene una mejor optimización disponible, sync.Poolpuede ayudar. (CloudFlare publicó una útil (pre sync.Pool) publicación de blog sobre reciclaje).

Finalmente, sobre si sus divisiones deben ser punteros: las divisiones de valores pueden ser útiles y ahorrarle asignaciones y errores de caché. Puede haber bloqueadores:

  • La API para crear sus elementos podría forzarle punteros, por ejemplo, debe llamar en NewFoo() *Foolugar de dejar que Go se inicialice con el valor cero .
  • La vida útil deseada de los artículos podría no ser la misma. Todo el corte se libera de una vez; Si el 99% de los elementos ya no son útiles, pero tiene punteros al otro 1%, toda la matriz permanece asignada.
  • Mover elementos puede causarle problemas. En particular, appendcopia elementos cuando crece la matriz subyacente . Los punteros que obtuvo antes del appendpunto en el lugar equivocado después, la copia puede ser más lenta para grandes estructuras y, por ejemplo, sync.Mutexno está permitido copiar. Insertar / eliminar en el medio y ordenar de forma similar mover elementos.

En términos generales, los segmentos de valor pueden tener sentido si coloca todos sus elementos en su lugar y no los mueve (por ejemplo, no más appendsegundos después de la configuración inicial), o si continúa moviéndolos pero está seguro de que es OK (no / uso cuidadoso de punteros a elementos, los elementos son lo suficientemente pequeños como para copiar de manera eficiente, etc.). A veces tienes que pensar o medir los detalles de tu situación, pero esa es una guía aproximada.


12
¿Qué significa grandes estructuras? ¿Hay un ejemplo de una estructura grande y una estructura pequeña?
El usuario sin sombrero

1
¿Cómo le dices a bytes.Replace toma 80 bytes de args en amd64?
Tim Wu

2
La firma es Replace(s, old, new []byte, n int) []byte; s, old y new son tres palabras cada una (los encabezados de división son(ptr, len, cap) ) y n intes una palabra, entonces 10 palabras, que a ocho bytes / palabra son 80 bytes.
twotwotwo

66
¿Cómo define grandes estructuras? ¿Qué tan grande es grande?
Andy Aldo

3
@AndyAldo Ninguna de mis fuentes (comentarios de revisión de código, etc.) define un umbral, por lo que decidí decir que es una decisión de juicio en lugar de aumentar el umbral. Tres palabras (como un segmento) se tratan de manera bastante consistente como elegibles para ser un valor en stdlib. Encontré una instancia de un receptor de valor de cinco palabras en este momento (texto / escáner.Posición) pero no leería mucho sobre eso (¡también se pasa como un puntero!). En ausencia de puntos de referencia, etc., simplemente haría lo que parezca más conveniente para la legibilidad.
twotwotwo

10

Tres razones principales por las que desearía utilizar receptores de métodos como punteros:

  1. "Primero, y lo más importante, ¿necesita el método modificar el receptor? Si lo hace, el receptor debe ser un puntero".

  2. "La segunda es la consideración de la eficiencia. Si el receptor es grande, una estructura grande, por ejemplo, será mucho más barato usar un receptor de puntero".

  3. "Lo siguiente es la coherencia. Si algunos de los métodos del tipo deben tener receptores de puntero, el resto también debería hacerlo, por lo que el conjunto de métodos es coherente independientemente de cómo se use el tipo".

Referencia: https://golang.org/doc/faq#methods_on_values_or_pointers

Editar: Otra cosa importante es saber el "tipo" real que está enviando para funcionar. El tipo puede ser un 'tipo de valor' o 'tipo de referencia'.

Incluso cuando las secciones y los mapas actúan como referencias, es posible que deseemos pasarlos como punteros en escenarios como cambiar la longitud de la sección en la función.


1
Para 2, ¿cuál es el límite? ¿Cómo sé si mi estructura es grande o pequeña? Además, ¿hay una estructura que sea lo suficientemente pequeña como para que sea más eficiente usar un valor en lugar de un puntero (para que no tenga que ser referenciado desde el montón)?
zlotnika

Yo diría que cuanto mayor sea el número de campos y / o estructuras anidadas dentro, mayor será la estructura. No estoy seguro de si hay un punto de corte específico o una forma estándar de saber cuándo una estructura puede llamarse "grande" o "grande". Si estoy usando o creando una estructura, sabría si es grande o pequeña según lo que dije anteriormente. ¡Pero solo soy yo!
Santosh Pillai

2

Un caso en el que generalmente necesita devolver un puntero es al construir una instancia de algún recurso con estado o compartible . Esto a menudo se realiza mediante funciones con el prefijo New.

Debido a que representan una instancia específica de algo y pueden necesitar coordinar alguna actividad, no tiene mucho sentido generar estructuras duplicadas / copiadas que representen el mismo recurso, por lo que el puntero devuelto actúa como el controlador del recurso en sí .

Algunos ejemplos:

En otros casos, los punteros se devuelven solo porque la estructura puede ser demasiado grande para copiar de forma predeterminada:


Alternativamente, se podrían evitar los punteros directamente al devolver una copia de una estructura que contiene el puntero internamente, pero tal vez esto no se considere idiomático:


Implícito en este análisis es que, por defecto, las estructuras se copian por valor (pero no necesariamente sus miembros indirectos).
nobar

2

Si puede (por ejemplo, un recurso no compartido que no necesita pasarse como referencia), use un valor. Por las siguientes razones:

  1. Su código será más agradable y más legible, evitando operadores de puntero y comprobaciones nulas.
  2. Su código será más seguro contra el pánico de puntero nulo.
  3. Su código será a menudo más rápido: sí, ¡más rápido! ¿Por qué?

Razón 1 : asignará menos elementos en la pila. La asignación / desasignación de la pila es inmediata, pero la asignación / desasignación en el montón puede ser muy costosa (tiempo de asignación + recolección de basura). Puede ver algunos números básicos aquí: http://www.macias.info/entry/201802102230_go_values_vs_references.md

Razón 2 : especialmente si almacena valores devueltos en segmentos, los objetos de su memoria estarán más compactados en la memoria: hacer un bucle en un segmento donde todos los elementos son contiguos es mucho más rápido que iterar un segmento donde todos los elementos son punteros a otras partes de la memoria . No para el paso de indirección sino para el aumento de errores de caché.

Rompemitos : una línea típica de caché x86 tiene 64 bytes. La mayoría de las estructuras son más pequeñas que eso. El momento de copiar una línea de caché en la memoria es similar a copiar un puntero.

Solo si una parte crítica de su código es lenta, probaría alguna microoptimización y comprobaría si el uso de punteros mejora un poco la velocidad, a costa de una menor legibilidad y mantenibilidad.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.