En Go hay varias formas de devolver un struct
valor 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 )?
new(MyStruct)
:) Pero en realidad no hay diferencia entre los diferentes métodos para asignar punteros y devolverlos.Respuestas:
tl; dr :
Un caso en el que a menudo debe usar un puntero:
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.bytes.Replace
toma 10 palabras de args (tres rebanadas y unaint
).Para sectores , no necesita pasar un puntero para cambiar elementos de la matriz.
io.Reader.Read(p []byte)
cambia los bytes dep
, 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
append
aceptar 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.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 ).
flag.StringVar
toma una*string
por 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
x
que se modifiquen muchos métodosx
, 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:
bytes.Buffer
.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.existingUser.LoadFromJSON(json []byte) error
podría envolverseNewUserFromJSON(json []byte) (*User, error)
. Nuevamente, empuja la elección entre pereza y asignaciones pellizcadas a la persona que llama.sync.Pool
manejen 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.Pool
puede ayudar. (CloudFlare publicó una útil (presync.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:
NewFoo() *Foo
lugar de dejar que Go se inicialice con el valor cero .append
copia elementos cuando crece la matriz subyacente . Los punteros que obtuvo antes delappend
punto en el lugar equivocado después, la copia puede ser más lenta para grandes estructuras y, por ejemplo,sync.Mutex
no 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
append
segundos 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.fuente
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)
) yn int
es una palabra, entonces 10 palabras, que a ocho bytes / palabra son 80 bytes.Tres razones principales por las que desearía utilizar receptores de métodos como punteros:
"Primero, y lo más importante, ¿necesita el método modificar el receptor? Si lo hace, el receptor debe ser un puntero".
"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".
"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.
fuente
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:
func NewTLSServer(handler http.Handler) *Server
- instanciar un servidor web para probarfunc Open(name string) (*File, error)
- devolver un identificador de acceso a archivosEn otros casos, los punteros se devuelven solo porque la estructura puede ser demasiado grande para copiar de forma predeterminada:
func NewRGBA(r Rectangle) *RGBA
- asignar una imagen en la memoriaAlternativamente, 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:
fuente
Si puede (por ejemplo, un recurso no compartido que no necesita pasarse como referencia), use un valor. Por las siguientes razones:
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.
fuente