Estructuras en Go

Introduccion a las estructuras

Go no es un lenguaje orientado a objetos como lo es c++,java y c#.No tiene objeto ni herencia, por lo tanto no tiene muchos conceptos asociados a POO como polimorfismo y sobrecarga.

Lo que Go tiene son estructuras, las cuales pueden ser asociadas con metodos. Go tambien soporta una forma de composicion.En general esto resulta en codigo simple.

A pesar de que go no haga OO como estas acostumbrado, notaras muchas similitudes entre la definicion de una structura con las tipicas class. Un ejemplo simple es la siguiente estructura Saiyan.

type Saiyan struct {
  Name string
  Power int
}

Luego veremos como agregar un metodo a esta estructura.

Declaraciones e inicializaciones

La manera simple de crear un valor de nuestra estructura es:

goku := Saiyan{
    Name: "Goku",
    Power: 9000,
  }

La coma final en la estructura es requerida, sin ella el compilador dara un error. este tipo de reglas nos ayuda a ser consistentes y de a poco veras los beneficios por vos mismo.

No necesitamos setear todos los campos o incluso podemos omitir todos.

goku := Saiyan{}

// o sino 

goku := Saiyan{Name: "Goku"}
goku.Power = 9000

Las variables que no tengan un valor asignado tendran el zero value correspondiente. En este caso seria Name = "" y Power = 0.

Tambien podes omitr el nombre del campo e ir agregando los valores en el orden pre establecido, aunque por un tema de ser explicito y claro esto solo lo deberias hacer con estrucuturas que tengan pocos campos.

goku := Saiyan{"Goku",9000}

Muchas veces no queremos que una variable este directamente asociada con un valor, sino que queremos que la variable contenga un puntero hacia el valor.

Un puntero es una direccion en memoria, esta direccion almacena el lugar donde encontraremos el valor actual.

Porque queremos un puntero al valor alamacenado en memoria en vez del valor actual limpia y llanamente? Esto se responde por como Go pasa los argumentos a las funciones: como copias.

Sabiendo esto, que imprime lo siguiente?

func main() {
  goku := Saiyan{"Goku", 9000}
  Super(goku)
  fmt.Println(goku.Power)
}

func Super(s Saiyan) {
  s.Power += 10000
}

La respuesta es 9000, porque no 19000?, Por que Super hace el cambio en la copia del valor original de goku entonces los cambios echos en Super no se ven reflejados en las lineas siguientes en el main().

Para hacer que esto funcione como esperamos, tenemos que pasar un puntero.

  func main() {
+   goku := &Saiyan{"Goku", 9000}
    Super(goku)
    fmt.Println(goku.Power)
  }
  
+   func Super(s *Saiyan) {
    s.Power += 10000
  }

Hicimos dos cambios. El primero usamos el operador & para obtener la direccion en memoria del valor.Luego, cambiamos el tipo del parametro que espera Super. Sigue esperando el valor de tipo Saiyan pero ahora espera una direccion de memoria *Saiyan . Hay una relacion entre Saiyan y *Saiyan, pero son dos tipos distintos.

Todavia estamos pasando una copia de goku a Super

Podemos demostrar que es una copia intentando cambiar a qué apunta (algo que probablemente no quieras hacer en realidad):

func main() {
    goku := &Saiyan{"Goku", 9000}
    Super(goku)
    fmt.Println(goku.Power)
}

func Super(s *Saiyan) {
-    s.Power += 10000
+    s = &Saiyan{"Gohan", 1000}
}

Lo anterior sigue devolviendo 9000.

Si tenes una estructura de datos compleja en Go, como un objeto Saiyan, que tiene varios campos, como nombre, poder, nivel y otros datos relacionados con un personaje de anime. Esta estructura puede tener muchos bytes de datos, dependiendo de cuántos campos tenga y el tipo de datos que contienen.

En una máquina de 64 bits, un puntero generalmente tiene un tamaño de 64 bits, independientemente del tamaño de la estructura a la que apunta. Por lo tanto, copiar un puntero es muy eficiente en comparación con copiar toda la estructura.

Usar punteros es más eficiente cuando trabajas con estructuras de datos grandes y complejas, ya que evitas la sobrecarga de copiar esos datos. Además, los punteros te permiten compartir y modificar el mismo valor en diferentes partes de tu programa, lo que puede ser muy útil en situaciones donde quieres que múltiples partes interactúen con los mismos datos.

Funciones en estructuras

Podemos asociar metodos a estructuras:

type Saiyan struct {
  Name string
  Power int
}

func (s *Saiyan) Super(){
  s.Power += 10000
}

En el codigo anterior estamos diciendo que el typo *Saiyan es el receptor del metodo Super.

Podemos hacer la llamada a Super asi :

goku := &Saiyan{"Goku", 9001}
goku.Super()
fmt.Println(goku.Power) // will print 19001

Contructores

Las estrucuturas no tienen constructores. En su lugar crearemos una funcion que retorne una instancia deseada (como una fabrica).

funct NewSaiyan(name string, power int) *Saiyan {
    return &Saiyan{
        Name:name,
        Power,power,
      }
  }

No es obligatorio devolver un puntero:

funct NewSaiyan(name string, power int) Saiyan {
    return Saiyan{
        Name:name,
        Power,power,
      }
  }

new

A pesar de la falta de Contructores Go tiene una funcion incorporada que utiliza para asignar en memoria un tipo. El resultado de new(x) es el mismo que &x.

Esto depende de vos, pero mucha gente prefiere la primera opcion a que es mas facil de leer:

goku := new(Saiyan)
goku.name = "goku"
goku.power = 9001

//vs

goku := &Saiyan {
name: "goku",
power: 9000,
}

Campos de una estructura

En los campos podemos tener cualquier tipo de dato , incluso otras estructuras.

type Saiyan struct {
    Name string
    Power int
    Father *Saiyan
  }

El cual inicializamos asi:

gohan := &Saiyan{
    Name: "Gohan",
    Power: 1000,
    Father: &Saiyan{
        Name: "Goku",
        Power: 9001,
        Father: nil,
      },
  }

Esta manera tambien es valida:

goku:= &Saiyan{
   Name: "Goku",
   Power: 9001,
   Father: nil,
}

gohan := &Saiyan{
    Name: "Gohan",
    Power: 1000,
    Father: &goku,
  }


### Composicion

La composicion es el acto de incluir una estructura o tipo 
de datos en otra para construir una estructura mas compleja.

```go[class="line-numbers"]
type Person struct {
    Name string
  }

func (p *Person) Introduce() {
    fmt.Printf("Hola %s\n",p.Name)
  }

type Saiyan struct {
    *Person
    Power int
  }

// Lo usamos asi:

goku := &Saiyan{
    Person: &Person("Goku"),
    Power: 9001,
  }

goku.Introduce()

La estructura Saiyan tiene el campo *Person. Como no queremos tener un nombre de campo explicito, podemos usar los campos y funciones de la composicion directamente. Sin embargo el compilador de Go le dio un nombre al campo. Lo siguiente tambien es valido.

goku := &Saiyan{
  Person: &Person{"Goku"},
}

fmt.Println(goku.Name)
fmt.Println(goku.Person.Name)

¿Es la composición mejor que la herencia? Muchas personas creen que es una forma más sólida de compartir código. Al usar la herencia, tu clase está fuertemente acoplada a tu superclase y terminas enfocándote en la jerarquía en lugar del comportamiento.

Overloading

Aunque la sobrecarga no es específica de las estructuras, vale la pena mencionarla. En pocas palabras, Go no admite la sobrecarga de funciones. Por esta razón, verás (y escribirás) muchas funciones que se parecen a Load, LoadById, LoadByName y similares. Sin embargo, debido a que la composición implícita es en realidad solo un truco del compilador, podemos ‘sobrescribir’ las funciones de un tipo compuesto. Por ejemplo, nuestra estructura Saiyan puede tener su propia función Introduce():

func (s *Saiyan) Introduce() {
  fmt.Printf("Hi, I'm %s. Ya!\n", s.Name)
}

La version de la composicion aun puede ser accedida : s.Person.Introduce().

Informacion extra

Estrucuturas anonimas

cuando tienes una estructura (struct) que se utiliza solo para representar un solo valor o instancia y no necesitas reutilizar esa estructura en otro lugar, no es necesario darle un nombre. En cambio, puedes utilizar una estructura anónima, es decir, una estructura que no tiene un nombre asociado.

Esta técnica es comúnmente utilizada en lo que se llama “table-driven tests” o pruebas basadas en tablas. En este contexto, en lugar de definir una estructura con nombre para cada conjunto de datos que deseas probar, puedes utilizar una estructura anónima para representar esos datos de manera más concisa.

testData := []struct {
    input  int
    output int
}{
    {1, 2},
    {2, 4},
    {3, 6},
    // ...
}

Como comparar dos estructuras en Go?

Debido a que las estructuras en Go pueden estar formadas por diferentes tipos de campos, cuando hablamos de comparación, estamos intentando determinar si dos variables son del mismo tipo de estructura y tienen los mismos valores asignados en sus campos. En esencia, estamos verificando si las dos variables son esencialmente la misma estructura con los mismos datos almacenados en sus campos.

Supongamos que tenemos esta estructura

type Saiyan struct {
    Name string
    Power int
  }

Y tres instancias:

goku := Saiyan{
    Name: "Kakaroto",
    Power: 9000
  }


gohan := Saiyan{
    Name: "gohan",
    Power: 1000
  }


kakaroto := Saiyan{
    Name: "kakaroto",
    Power: 9000
  }

Como vemos la estrucuta goku y kakaroto son iguales, pero goku y gohan son diferentes. Hagamos la comparacion:

if goku != gohan {
    fmt.Println("Saiyan goku y gohan son diferentes")
}
if goku == kakaroto {
    fmt.Println("Saiyan goku y kakaroto son iguales")
}

No es posible hacer comparaciones entre diferentes tipos, si lo intentas la tiempo de compilar veras un error.

var numero int
var texto string

if numero == texto {
    // Esto generará un error en tiempo de compilación.
}

Por lo que se recomienda verificar que sean del mismo tipo antes de comparar sus valores con reflect.TypeOf(estructura).

import "reflect"

var numero int
var otroNumero int

if reflect.TypeOf(numero) == reflect.TypeOf(otroNumero) {
    // Ambos valores son del mismo tipo, puedes realizar la operación aquí.
} else {
    // Los valores son de tipos diferentes, realiza alguna acción adecuada.
}

Esta técnica se usa en casos en los que es importante verificar que los valores tengan el mismo tipo antes de realizar operaciones que dependen de la compatibilidad de tipos. Sin embargo, ten en cuenta que el uso excesivo de la reflexión en Go puede afectar el rendimiento y la legibilidad del código, por lo que se recomienda utilizarla con moderación y considerar alternativas siempre que sea posible.