Errores de Puntero nil
Introducción
Durante un proceso de escritura de código, necesité llamar al método Close() para cerrar múltiples objetos, como en el siguiente código:
type A struct {
b B
c C
d D
}
func (a A) Close() error {
if a.b != nil {
if err := a.b.Close(); err != nil {
return err
}
}
if a.c != nil {
if err := a.c.Close(); err != nil {
return err
}
}
if a.d != nil {
if err := a.d.Close(); err != nil {
return err
}
}
return nil
}Pero escribir tantos juicios if no parecía elegante. B, C y D todos implementan el método Close, debería poder ser más conciso. Así que los puse en un slice, y luego iteré:
func (a A) Close() error {
closers := []io.Closer{
a.b,
a.c,
a.d,
}
for _, closer := range closers {
if closer != nil {
if err := closer.Close(); err != nil {
return err
}
}
}
return nil
}Esto parece mejor. Entonces ejecutémoslo para ver:
func main() {
var a A
if err := a.Close(); err != nil {
panic(err)
}
fmt.Println("success")
}El resultado fue inesperado, ¡colapsó! El mensaje de error es el siguiente, significa que no se puede llamar a un método con un receptor nil. El if closer != nil en el bucle parece no haber funcionado como filtro:
panic: value method main.B.Close called using nil *B pointerEl ejemplo anterior es una versión simplificada de un bug que encontré una vez. Muchos principiantes pueden cometer este error al principio como yo. A continuación explicaré exactamente qué está pasando.
Interfaz
En capítulos anteriores se mencionó que nil es el valor cero de tipos de referencia, como slices, maps, channels, funciones, punteros e interfaces. Para slices, maps, channels y funciones, todos se pueden considerar como punteros que apuntan a la implementación concreta.

Pero solo la interfaz es diferente. La interfaz está compuesta por dos cosas: tipo y valor.

Cuando se intenta asignar nil a una variable, no pasará la compilación y mostrará la siguiente información:
use of untyped nil in assignmentEl contenido es básicamente que no se puede declarar una variable con valor untyped nil. Si existe untyped nil, relativamente definitivamente habrá typed nil, y esta situación a menudo ocurre con interfaces. Veamos un ejemplo simple:
func main() {
var p *int
fmt.Println(p)
fmt.Println(p == nil)
var pa any
pa = p
fmt.Println(pa)
fmt.Println(pa == nil)
}Salida:
<nil>
true
<nil>
falseEl resultado es muy extraño. Claramente la salida de pa es nil, pero no es igual a nil. Podemos usar reflexión para ver qué es realmente:
func main() {
var p *int
fmt.Println(p)
fmt.Println(p == nil)
var pa any
pa = p
fmt.Println(reflect.TypeOf(pa))
fmt.Println(reflect.ValueOf(pa))
}Salida:
<nil>
true
*int
<nil>Desde el resultado se puede ver que es (*int)(nil). Es decir, pa almacena el tipo *int, y su valor real es nil. Cuando se realiza una operación de igualdad en un valor de tipo interfaz, primero se juzga si sus tipos son iguales. Si los tipos no son iguales, se determina directamente como no iguales. Luego se juzga si los valores son iguales. La lógica de juicio de interfaz en esta parte se puede referir a la función cmd/compile/internal/walk.walkCompare.
Por lo tanto, si se desea que una interfaz sea igual a nil, debe tener su valor como nil y su tipo también como nil, porque el tipo en la interfaz es realmente un puntero:
type iface struct {
tab *itab
data unsafe.Pointer
}Si se desea evitar el tipo y juzgar directamente si su valor es nil, se puede usar reflexión. Aquí hay un ejemplo:
func main() {
var p *int
fmt.Println(p)
fmt.Println(p == nil)
var pa any
pa = p
fmt.Println(reflect.ValueOf(pa).IsNil())
}A través de IsNil() se puede juzgar directamente si su valor es nil. De esta manera no ocurrirá el problema anterior. Por lo tanto, durante el uso normal, si el valor de retorno de una función es un tipo de interfaz, y se desea retornar un valor cero, es mejor retornar directamente nil, no retornar ningún valor cero de implementación concreta. Aunque implemente la interfaz, nunca será igual a nil, lo que puede causar el error del ejemplo.
Resumen
Después de resolver el problema anterior, veamos los siguientes ejemplos:
Cuando el receptor de un método de una estructura es un receptor de puntero, nil es usable. Veamos el siguiente ejemplo:
type A struct {
}
func (a *A) Do() {
}
func main() {
var a *A
a.Do()
}Este código puede ejecutarse normalmente sin reportar errores de puntero nulo.
Cuando un slice es nil, se puede acceder a su longitud y capacidad, y también se le pueden agregar elementos:
func main() {
var s []int
fmt.Println(len(s))
fmt.Println(cap(s))
s = append(s, 1)
}Cuando un map es nil, todavía se puede acceder a él, pero el map nil es de solo lectura. Una vez que se intenta escribir, se producirá un panic:
func main() {
var s map[string]int
i, ok := s[""]
fmt.Println(i, ok)
fmt.Println(len(s))
// Al intentar escribir, se producirá un panic
s["a"] = 1 // panic: assignment to entry in nil map
}Estas características relacionadas con nil en los ejemplos anteriores pueden ser confusas, especialmente para los principiantes de Go. nil representa el valor cero de los tipos mencionados anteriormente, es decir, el valor predeterminado. El valor predeterminado debe mostrar un comportamiento predeterminado. Esto es exactamente lo que los diseñadores de Go querían ver: hacer que nil sea más útil, en lugar de lanzar directamente errores de puntero nulo. Esta filosofía también se refleja en la biblioteca estándar. Por ejemplo, para iniciar un servidor HTTP se puede escribir así:
http.ListenAndServe(":8080", nil)Podemos pasar directamente un nil Handler, y luego la biblioteca http usará el Handler predeterminado para manejar solicitudes HTTP.
TIP
Los interesados pueden ver este video Understanding nil - Gopher Conference 2016, es muy claro y fácil de entender.
