Jesús Mendoza

Notas de Go - Pointers, Structs e Interfaces

Pointers

Los pointers en Go son variables que almacenan la dirección en memoria de otra variable. Cada vez que creamos una variable de cualquier tipo se crea un segmento en memoria en donde se almacena dicho valor, Go, al ser un lenguaje en donde los valores de variables se pasan por valor en vez de por referencia, no tiene ningúna forma de saber el lugar original en memoria donde se encuentra la variable.

Pasar por valor o pasar por referencia

Pasar por valor significa que cada vez que pasamos una variable a una función, una expresión, etc. el valor de esa variable se sustituye con su valor. Pasar por referencia significa que cada vez que pasamos una variable a una función, una expresión, etc. el valor se pasa con su dirección en memoria (pointer), por lo que si modificamos dicho valor se está modificando el valor original (que existe en dicha dirección de memoria),

El operador * y &

En Go, un pointer es representado usando * (asterisco) seguido del tipo del valor almacenado. Pero el * (asterisco) también se usa para "dereferenciar" una variable de tipo pointer, lo que nos da acceso al valor al cual el pointer está apuntando.

El operador & se usa para encontrar la dirección en memoria en la cual una variable está siendo almacenada.

x := 10
y := &x

fmt.Printf("%T", y) // *int

El tipo de &x es *int (puntero a un int) por que el tipo de la variable x es un int, si dicho tipo fuese un string retornaría *string y así sucesivamente. Esto es lo que nos permite modificar el valor de una variable, cómo mencioné anteriormente, en Go las variables se pasan por valor y no por referencia pero al pasarle la referencia de memoria podemos modificar y reemplazar dicho valor directamente en memoria.

New

Otra forma de crear un puntero es usando la palabra reservada new.

func modificarValor(value *int) {
  value = 10
}

func main() {
  puntero := new(int)

  modificarValor(puntero)

  fmt.Println(*puntero)
}

En el código anterior estamos creando un puntero de tipo *int y almacenando la dirección en memoria en la variable puntero, luego llamamos a la función modificarValor y le pasamos puntero como argumento (cuyo valor es la dirección en memoria de nuestro nuevo *int), si dicho valor no fuese un pointer se reemplazaría con el valor almacenado en la variable pero en este caso pasamos una referencia en memoria lo que nos permite modificar el valor original en la función modificarValor.

Structs

Un struct es una estructura de datos que tiene campos con nombres (algo parecido a un objecto en JavaScript o un diccionario en Python) y similar a un map de Go. La diferencia principal con un map de Go es que dentro de un struct se pueden definir diferentes tipos de datos mientras que en un map siempre son de un mismo tipo.

type Direccion struct {
  calle   string
  numero  int
}

type Persona struct {
  nombre          string
  apellido        string
  edad            int
  direccion       Direccion
  juegosFavoritos []string
}

func main() {
  persona := Persona{
    nombre:   "Jesus",
    apellido: "Mendoza",
    edad:     29,
    direccion: Direccion{
      calle: "Calle loca",
    },
    juegosFavoritos: []string{
      "League of Legends",
      "Call of Duty",
    },
  }

  fmt.Println(persona)
}

En el ejemplo anterior tenemos un type Direccion struct que representa un struct donde almacenaremos nuestra dirección y como podemos observar tiene dos tipos distintos, luego tenemos un type Persona struct que representa una persona y como podemos observar tiene diferentes tipos, incluso en el campo direccion le pasamos Direccion para definir que queremos que ese campo tenga la estructura del struct Direccion y en juegosFavoritos tenemos un slice de tipos string.

Hemos visto que podemos asignarle campos de cualquier tipo a los struct, incluso podemos asignarle métodos de la siguiente manera:

type Direccion struct {
  calle   string
  numero  int
}

func (d *Direccion) imprimirDireccion() {
  direccion := fmt.Sprintln(d.calle, "numero:", d.numero)

  fmt.Println(direccion)
}

func main() {
  direccion := Direccion{
    calle: "Calle loca",
    numero: 53,
  }

  direccion.imprimirDireccion() // Calle local número: 53
}

Al crear la función usando (d *Direccion) básicamente le estamos diciendo a nuestro programa que todas las instancias creadas usando el struct Dirección tengan ese método asignado a ellas.

Interfaces

Las interfaces nos permiten escribir código más modular, reutilizable y flexible en Go. Nos permite hacer composición, es decir, combinar tipos de datos para formar otros más complejos. Las interfaces definen un comportamiento de un tipo. Por ejemplo:

type Article struct {
  titulo   string
  autor    string
}

func (a Article) ToString() string {
  return fmt.Sprintf("El artículo %q fue escrito por %s.", a.titulo, a.autor)
}

func main() {
  a := Article{
    title: "Notas de Go",
    author: "Jesus Mendoza",
  }

  fmt.Println(a.ToString())
}

En el ejemplo anterior estamos creando un type Article struct y definiendo sus propiedades, luego le asignamos un método ToString el cual nos imprime un string que concatena el título y el autor, luego si queremos usar dicho método lo hacemos usando a.ToString().

Para compartir funcionalidad con una interface podemos hacer lo siguiente:

type StringConverter interface {
  ToString() string
}

type Article struct {
  titulo   string
  autor    string
}

func (a Article) ToString() string {
  return fmt.Sprintf("El artículo %q fue escrito por %s.", a.titulo, a.autor)
}

func main() {
  a := Article{
        titulo:  "Notas de Go",
        autor: "Jesus Mendoza",
    }

  Print(a)
}

func Print(s StringConverter) {
    fmt.Println(s.ToString())
}

En el ejemplo anterior podemos ver que nuestro código es similar, la única diferencia es que agregamos una función nueva llamada Print que acepta una interface llamada StringConverter y lo único que hace la función Print es invocar el método ToString de nuestra interface StringConverter. Debido a que el compilador de Go reconoce que StringConverter es una interface hará que acepte solo como argumento los tipos que tengan un método ToString, es decir, si le pasamos un struct que no tiene un método toString nuestro programa fallará. Por ejemplo:

  type StringConverter interface {
    ToString() string
  }

  type Article struct {
    titulo string
    autor  string
  }

  type ArticleWithoutToStringMethod struct {
    titulo string
    autor string
  }

  func (a Article) ToString() string {
    return fmt.Sprintf("El artículo %q fue escrito por %s.", a.titulo, a.autor)
  }

  func main() {
    a := Article{
      titulo: "Notas de Go",
      autor:  "Jesus Mendoza",
    }
    b := ArticleWithoutToStringMethod{
      titulo: "Notas de Go",
      autor:  "Jesus Mendoza",
    }

    Print(a)
    Print(b)
  }

  func Print(s StringConverter) {
    fmt.Println(s.ToString())
  }

En el ejemplo anterior Print(b) fallará por que nuestro type ArticleWithoutToStringMethod struct no tiene ningún método ToString asignado.

En el próximo post hablarémos sobre concurrencia en Go.