En marzo de 2012, la cantante mexicana Paulina Rubio publicó un nuevo single llamado “Boys Will Be Boys”, título que podríamos traducir a “Los chicos serán chicos”. En ella, Paulina habla de una historia de amor a la vez que cuenta algunas de las particularidades de los chicos, desde su punto de vista. Hoy, con “Los nil serán nil”, os queremos contar nuestra historia de amor con Go y, más concretamente, las particularidades de los nil.
En Go, nil
es el valor cero para punteros, interfaces, mapas, slices, canales y funciones; y corresponde a la
representación de un valor no inicializado. Pero ojo, es muy importante no confundir valor “no inicializado” con estado
indeterminado, pues nil
no es más que otro posible valor válido. Si, por ejemplo, hablamos de la interfaz
error, el valor nil
será el equivalente a decir que no hay error. Otra
cosa muy diferente sería decir que no sabemos si hay o no error (estado indeterminado).
Entendiendo la nulabilidad
Es muy habitual hablar de “tipos nulables” en lenguajes como C#, PHP o Kotlin, para hacer referencia a tipos que pueden tener un valor inicializado o simplemente no tenerlo. Por ejemplo, podríamos ver las siguientes funciones:
fun foo(): Fooable? // Kotlin
public Nullable<Fooable> Foo() // C#
function foo(): ?Fooable; // PHP
Sin embargo, sobretodo cuándo hablamos de lenguajes compilados y fuertemente tipados, es habitual que no todos los tipos
sean nulables, o lo que sería lo mismo, el valor null
o nil
no es un valor válido para todos los tipos. Eso es, si,
por ejemplo, hablamos del tipo entero, o del tipo string, y pensamos en el dominio de posibles valores que estos tipos
pueden tener, veremos que el valor null
o nil
no tiene cabida. En su defecto, la representación de una variable (de estos tipos)
que no esté inicializada, se suele asociar a determinados valores válidos. Por ejemplo, para los enteros se suele usar el
cero (0) y para los string se suele usar la cadena vacía ("").
Hasta aquí todo parece claro, pero, por desgracia, seguimos sin tener forma de definir cuándo un valor no está inicializado. Lo comentado anteriormente nos puede salvar en determinadas circunstancias, sin embargo, habrá veces dónde vamos a querer que cero o la cadena vacía sean valores válidos que representen una variable sí inicializada.
Entonces, ¿cómo lo hacemos?
La nulabilidad en Go
Como ya dijimos anteriormente, el valor nil
es un valor válido para algunos de los tipos soportados por el lenguaje Go.
Pero, si habéis prestado atención, seguramente os habéis dado cuenta de que entre esos tipos que permiten nil
como un
valor válido no había ningún tipo básico (enteros, coma flotante, boleanos, etc) ni tampoco un tipo tan habitual como lo
son los structs.
Así, ¿cómo solucionamos la representación de variables no inicializadas de estos tipos?
Los punteros y las interfaces son la respuesta.
De modo que, cuándo tengamos que definir una variable de un tipo no nulable, y tengamos la necesidad de definir que esa variable podrá estar o no inicializada, lo que haremos será usar (mayormente) un puntero del mismo tipo que la variable.
Veamos un pequeño ejemplo:
package main
import (
"fmt"
)
type Gopher struct {
Name string
Color string
Size float64
}
func main() {
var gopher Gopher
var gopherP *Gopher
fmt.Println(gopher == nil)
fmt.Println(gopherP == nil)
fmt.Println("Hello, playground")
}
Si intentáis ejecutar este ejemplo en el Playground veréis que os da un error de compilación.
La razón: el tipo Gopher
(que es un typed struct) de la variable gopher
no permite nil
como un posible valor válido.
Sin embargo, ese mismo problema no lo tendríamos con la variable gopherP
, pues, como podemos ver, es un puntero a un
tipo Gopher
, y, como vimos anteriormente, los punteros sí que permiten nil
como un valor válido.
En caso de que no queramos exponer nuestras estructuras de datos y solo firmar los contratos con los consumidores de nuestro paquete, entonces podremos recurrir a las interfaces como forma de representación de ese estado “no inicializado”. El mejor ejemplo de esto, es el ya comentado anteriormente: error.
A quién no le suena esto:
if err != nil {
// error handling
}
En ésta ocasión, la mayoría de veces estamos haciendo referencia a la interfaz error sin tener la necesidad de conocer en detalle la estructura de datos que representa el error, y, a su vez, pudiendo determinar si tenemos o no error.
Recuerda: los nil son solo un valor más
En efecto, no debemos olvidar en ningún momento que nil
no es más que otro posible valor para algunos de los tipos.
De hecho, si recordamos que una variable suele tener asociado un tipo y valor, entonces podremos hacer la
siguiente prueba:
package main
import (
"fmt"
)
func main() {
var integer *int
var empty interface{}
fmt.Println(integer == nil) // Prints: true
fmt.Println(empty == nil) // Prints: true
fmt.Println(integer == empty) // Prints: false
}
Ambas variables (integer
y empty
) tienen el valor nil
sin embargo sus tipos son distintos (*int
y interface{}
respectivamente).
Este mismo ejemplo, lo podemos hacer un poco más complejo aún.
package main
import (
"fmt"
)
func main() {
var integer *int
var empty interface{} = integer
fmt.Println(integer == nil) // Prints: true
fmt.Println(empty == nil) // Prints: false
fmt.Println(integer == empty) // Prints: true
}
¿Qué es lo que ocurre ahora?
Seguramente os esté pareciendo raro el resultado, pero, aún os lo parecerá más si probáis de ejecutar la siguiente linea a continuación:
fmt.Printf("a=(%T,%v)\n", empty, empty) // Prints: a=(*int,<nil>)
¡Sorprendente, eh!
El valor de la variable es efectivamente nil
sin embargo la comparación con dicho valor no nos dice lo mismo. Y ésta es,
precisamente, la magia del compilador, a la hora de gestionar las particularidades del nil
que explicamos anteriormente.
Pues, en esta ocasión, la variable empty
puede tener nil
como un valor válido, porqué es un puntero, pero, más adelante
en nuestro código, podríamos asignar nuestra variable empty
a una variable de un tipo que no acepte nil
como valor
válido. Es por eso que, en tiempo de compilación, éste debe asumir determinados aspectos para tomar una decisión.
Así que, id con cuidado y recordad: nil
es solo un valor más, pero no es un valor válido para todos los tipos.
Y vosotros, ¿ya teníais claro cuál es el funcionamiento de los nil
?
Como siempre, estaremos encantados de recibir vuestro feedback en los comentarios del blog o vía Twitter.