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.