Estructuras en Go
Las estructuras pueden almacenar un grupo de datos de diferentes tipos, es un tipo compuesto. Go abandonó las clases y la herencia, también abandonó los constructores, debilitando deliberadamente la funcionalidad orientada a objetos. Go no es un lenguaje OOP tradicional, pero todavía tiene rastros de OOP, a través de estructuras y métodos también se puede simular una clase. A continuación un ejemplo simple de una estructura:
type Programmer struct {
Name string
Age int
Job string
Language []string
}Declaración
La declaración de una estructura es muy simple, por ejemplo:
type Person struct {
name string
age int
}Tanto la estructura en sí como sus campos internos siguen la convención de exposición mediante mayúsculas y minúsculas. Para algunos campos adyacentes del mismo tipo, no es necesario repetir la declaración del tipo, por ejemplo:
type Rectangle struct {
height, width, area int
color string
}TIP
Al declarar campos de una estructura, el nombre del campo no puede repetirse con el nombre de un método.
Instanciación
Go no tiene constructores, en la mayoría de los casos se usa la siguiente forma para instanciar una estructura. Al inicializar, como con map, se especifica el nombre del campo y luego se inicializa el valor del campo:
programmer := Programmer{
Name: "jack",
Age: 19,
Job: "coder",
Language: []string{"Go", "C++"},
}Sin embargo, también se pueden omitir los nombres de los campos. Cuando se omiten los nombres de los campos, se deben inicializar todos los campos. Normalmente no se recomienda usar esta forma porque la legibilidad es mala.
programmer := Programmer{
"jack",
19,
"coder",
[]string{"Go", "C++"}}Si el proceso de instanciación es complejo, también se puede escribir una función para instanciar la estructura, como se muestra a continuación. También se puede considerar como un constructor:
type Person struct {
Name string
Age int
Address string
Salary float64
}
func NewPerson(name string, age int, address string, salary float64) *Person {
return &Person{Name: name, Age: age, Address: address, Salary: salary}
}Sin embargo, Go no soporta sobrecarga de funciones y métodos, por lo que no se pueden definir diferentes parámetros para la misma función o método. Si se quiere instanciar una estructura de múltiples formas, se pueden crear múltiples constructores o se recomienda usar el patrón options.
Patrón de opciones
El patrón de opciones es un patrón de diseño muy común en Go, permite instanciar estructuras de manera más flexible, con alta extensibilidad, y sin necesidad de cambiar la firma de la función del constructor. Supongamos la siguiente estructura:
type Person struct {
Name string
Age int
Address string
Salary float64
Birthday string
}Declaramos un tipo PersonOptions, acepta un parámetro de tipo *Person, debe ser un puntero porque queremos asignar valores a Person dentro de un closure.
type PersonOptions func(p *Person)A continuación creamos funciones de opción, generalmente comienzan con With, su valor de retorno es una función closure.
func WithName(name string) PersonOptions {
return func(p *Person) {
p.Name = name
}
}
func WithAge(age int) PersonOptions {
return func(p *Person) {
p.Age = age
}
}
func WithAddress(address string) PersonOptions {
return func(p *Person) {
p.Address = address
}
}
func WithSalary(salary float64) PersonOptions {
return func(p *Person) {
p.Salary = salary
}
}La firma real del constructor es la siguiente, acepta un parámetro de longitud variable de tipo PersonOptions.
func NewPerson(options ...PersonOptions) *Person {
// aplicar primero las options
p := &Person{}
for _, option := range options {
option(p)
}
// manejo de valores por defecto
if p.Age < 0 {
p.Age = 0
}
......
return p
}De esta manera, para diferentes necesidades de instanciación solo se necesita un constructor, pasando diferentes funciones Options:
func main() {
pl := NewPerson(
WithName("John Doe"),
WithAge(25),
WithAddress("123 Main St"),
WithSalary(10000.00),
)
p2 := NewPerson(
WithName("Mike jane"),
WithAge(30),
)
}El patrón de opciones funcionales se puede ver en muchos proyectos de código abierto, la instanciación de gRPC Server también usa este patrón de diseño. El patrón de opciones funcionales solo es adecuado para instanciaciones complejas, si solo hay algunos parámetros simples, se recomienda usar un constructor normal.
Composición
En Go, las relaciones entre estructuras se representan mediante composición, puede ser composición explícita o composición anónima, el último se usa más como herencia, pero esencialmente no hay ningún cambio. Por ejemplo:
Forma de composición explícita:
type Person struct {
name string
age int
}
type Student struct {
p Person
school string
}
type Employee struct {
p Person
job string
}Al usarla se debe especificar explícitamente el campo p:
student := Student{
p: Person{name: "jack", age: 18},
school: "lili school",
}
fmt.Println(student.p.name)La composición anónima no necesita especificar explícitamente el campo:
type Person struct {
name string
age int
}
type Student struct {
Person
school string
}
type Employee struct {
Person
job string
}El nombre del campo anónimo por defecto es el nombre del tipo, el llamador puede acceder directamente a los campos y métodos de ese tipo, pero aparte de ser más conveniente, no hay diferencia con la primera forma.
student := Student{
Person: Person{name: "jack",age: 18},
school: "lili school",
}
fmt.Println(student.name)Punteros
Para punteros a estructuras, no es necesario desreferenciar para acceder directamente al contenido de la estructura, por ejemplo:
p := &Person{
name: "jack",
age: 18,
}
fmt.Println(p.age,p.name)En tiempo de compilación se convertirá a (*p).name, (*p).age, en realidad todavía se necesita desreferenciar, pero al codificar se puede omitir, es una especie de azúcar sintáctico.
Etiquetas
Las etiquetas de estructura son una forma de metaprogramación, combinadas con reflexión pueden lograr muchas funciones sorprendentes. El formato es el siguiente:
`key1:"val1" key2:"val2"`Las etiquetas son una forma de pares clave-valor, separadas por espacios. La tolerancia a errores de las etiquetas de estructura es muy baja, si no se escribe la estructura en el formato correcto, no se podrá leer normalmente, pero en tiempo de compilación no habrá ningún error. A continuación un ejemplo de uso:
type Programmer struct {
Name string `json:"name"`
Age int `yaml:"age"`
Job string `toml:"job"`
Language []string `properties:"language"`
}La aplicación más amplia de las etiquetas de estructura es en la definición de alias en varios formatos de serialización, el uso de etiquetas debe combinarse con reflexión para aprovechar completamente su funcionalidad.
Alineación de memoria
La distribución de memoria de los campos de estructura en Go sigue las reglas de alineación de memoria, esto puede reducir el número de veces que la CPU accede a la memoria, a cambio ocupa algo más de memoria, es un medio de intercambiar espacio por tiempo. Supongamos la siguiente estructura:
type Num struct {
A int64
B int32
C int16
D int8
E int32
}Se conoce el número de bytes que ocupan estos tipos:
int64ocupa 8 bytesint32ocupa 4 bytesint16ocupa 2 bytesint8ocupa 1 byte
¿El uso de memoria de toda la estructura parece ser 8+4+2+1+4=19 bytes? Por supuesto no es así, según las reglas de alineación de memoria, el uso de memoria de una estructura es al menos un múltiplo entero del campo más grande, si no es suficiente se rellena. En esta estructura el más grande es int64 que ocupa 8 bytes, entonces la distribución de memoria es como se muestra en la siguiente figura:

Por lo tanto, en realidad ocupa 24 bytes, de los cuales 5 bytes son inútiles.
Veamos ahora esta otra estructura:
type Num struct {
A int8
B int64
C int8
}Entendidas las reglas anteriores, se puede entender rápidamente que su uso de memoria también es 24 bytes, aunque solo tiene tres campos, desperdicia 14 bytes.

Pero podemos ajustar los campos, cambiando al siguiente orden:
type Num struct {
A int8
C int8
B int64
}Así el uso de memoria se convierte en 16 bytes, desperdiciando 6 bytes, reduciendo el desperdicio de memoria en 8 bytes.

Teóricamente, hacer que los campos de una estructura se distribuyan en un orden razonable puede reducir su uso de memoria. Sin embargo, en el proceso real de codificación, no hay una razón necesaria para hacerlo, no necesariamente traerá mejoras sustanciales en la reducción del uso de memoria, pero seguro que aumentará la presión arterial y la carga mental de los desarrolladores, especialmente en negocios donde algunas estructuras pueden tener docenas o cientos de campos, por lo que basta con conocerlo.
TIP
Si realmente quieres usar este método para ahorrar memoria, puedes ver estas dos bibliotecas:
Verificarán las estructuras en tu código fuente, calcularán y reorganizarán los campos de las estructuras para minimizar el uso de memoria de las estructuras.
Estructura vacía
Una estructura vacía no tiene campos, no ocupa espacio de memoria, podemos calcular el tamaño en bytes que ocupa mediante la función unsafe.SizeOf:
func main() {
type Empty struct {}
fmt.Println(unsafe.Sizeof(Empty{}))
}Salida
0Hay muchos escenarios de uso para estructuras vacías, como se mencionó anteriormente, como tipo de valor de un map, se puede usar el map como un set, o como tipo de un canal, representando un canal solo de notificación.
