Переоцінені можливості nil в Go або Nil is not nil
Привіт! У цій статті розглянемо помилку перевірки на nil в Go, причини помилки та варіанти виправлення. Стаття буде корисна гоферам-початківцям.
Nil
Nil в Go має широкі можливості для використання, простіше це побачити на прикладі:
package main
import "fmt"
type Tree struct {
left *Tree
right *Tree
value int
}
func (t *Tree) Sum() int {
if t == nil {
return 0
}
return t.value + t.left.Sum() + t.right.Sum()
}
func main() {
{
var data map[string]string = nil
fmt.Printf("map[string]string len(data) = %d\n", len(data))
for key, value := range data {
fmt.Printf("key = %q, value = %q\n", key, value)
}
}
{
var data []string = nil
fmt.Printf("[]string len(data) = %d, cap(data) = %d\n", len(data), cap(data))
for key, value := range data {
fmt.Printf("key = %d, value = %q\n", key, value)
}
}
{
var tree *Tree = nil
fmt.Printf("Tree.Sum() = %d\n", tree.Sum())
}
}go run ./examples/001-useful-nil/main.go
map[string]string len(data) = 0 []string len(data) = 0, cap(data) = 0 Tree.Sum() = 0
Якщо приклади зі слайсом та мапою зустрічаються доволі часто, то про можливості nil в структурі дізнався на другому році роботи з Go.
Nil is not nil
Інтерфейс в Go складається з полів type та value.
package main
import (
"fmt"
"unsafe"
)
type Company struct {
Name string
}
func main() {
var (
in interface{} = nil
company *Company = nil
)
fmt.Printf("in == nil %t\n", in == nil) // true
fmt.Printf("%v\n\n", (*[2]uintptr)(unsafe.Pointer(&in))) // &[0 0]
in = company
fmt.Printf("in == nil %t\n", in == nil) // false
fmt.Printf("in %v\n\n", (*[2]uintptr)(unsafe.Pointer(&in))) // &[4837344 0]
in = &Company{
Name: "Example",
}
fmt.Printf("in == nil %t\n", in == nil) // false
fmt.Printf("in %v\n\n", (*[2]uintptr)(unsafe.Pointer(&in))) // &[4837344 824633786896]
in = nil
fmt.Printf("in == nil %t\n", in == nil) // true
fmt.Printf("in %v\n\n", (*[2]uintptr)(unsafe.Pointer(&in))) // &[0 0]
}go run ./examples/003-interface-inside/main.go
in == nil true &[0 0] in == nil false in &[4837344 0] in == nil false in &[4837344 824633786896] in == nil true in &[0 0]
Схожий приклад «nil» Interfaces and «nil» Interfaces Values зустрічається в статті 50 Shades of Go:
Interface variables will be «nil» only when their type and value fields are «nil».
package main
import "fmt"
func main() {
var (
data *byte
in interface{}
)
fmt.Println(data, data == nil) // prints: <nil> true
fmt.Println(in, in == nil) // prints: <nil> true
in = data
fmt.Println(in, in == nil) // prints: <nil> false
// 'data' is 'nil', but 'in' is not 'nil'
}У таких випадках спроба використати методи спровокує паніку:
package main
import (
"testing"
"github.com/stretchr/testify/require"
)
type Company struct {
name string
}
func (c *Company) Name() string {
return c.name
}
type NameGetter interface {
Name() string
}
func TestNotNilPanic(t *testing.T) {
var (
compamy *Company = nil
in NameGetter = nil
)
require.True(t, compamy == nil)
require.True(t, in == nil)
in = compamy
require.True(t, in != nil)
require.Panics(t, func() {
if in != nil {
_ = in.Name()
}
})
}go test ./examples/004-non-nil-panic/main_test.go -v -count=1
=== RUN TestNotNilPanic --- PASS: TestNotNilPanic (0.00s) PASS ok command-line-arguments 0.003s
require.Panics очікує паніку.
Причини виникнення помилок з nil
Попередні приклади помилок були штучними та рідко зустрічаються в проєктах в такому простому виді.
Зазвичай, такі помилки замасковані обгорткою викликів:
package main
import (
"testing"
"github.com/stretchr/testify/require"
)
type ResponseError string
func (r ResponseError) Error() string {
return string(r)
}
func Handle() error {
var err *ResponseError = nil
// some logic
return err
}
func TestHandle(t *testing.T) {
require.True(t, Handle() != nil)
}go test ./examples/005-not-nil-error/... -v -count=1
=== RUN TestHandle --- PASS: TestHandle (0.00s) PASS
Або складніший варіант:
package main
import (
"testing"
"github.com/stretchr/testify/require"
)
type ResponseError string
func (r ResponseError) Error() string {
return string(r)
}
func Handle() *ResponseError {
var err *ResponseError = nil
// some logic
return err
}
func WrapHandle() error {
// some wrap logic
return Handle()
}
func TestWrapHandle(t *testing.T) {
require.True(t, Handle() == nil)
require.True(t, WrapHandle() != nil)
}go test ./examples/006-not-nil-wrap-error/... -v -count=1
=== RUN TestHandle --- PASS: TestWrapHandle (0.00s) PASS
Такі помилки часто асоціюються саме з інтерфейсом error, але також трапляються, коли потрібно явно привести сигнатуру функції.
Розглянемо на прикладі купівлі чистого повітря. У нас є різні провайдери, які продають чисте повітря, кожен провайдер має свій API. Спростив до одного провайдера:
package main
import (
"errors"
"testing"
"github.com/stretchr/testify/require"
)
var (
ErrOxygenSoldOut = errors.New("oxygen sold out")
)
type OxygenProviderResponse interface {
GetID() uint64
GetPrice() float64
}
type OxygenProvider = func(quantity float64) (OxygenProviderResponse, error)
type ForestOxygenProvider struct {
ID uint64
Price float64
}
func (f *ForestOxygenProvider) GetID() uint64 {
return f.ID
}
func (f *ForestOxygenProvider) GetPrice() float64 {
return f.Price
}
func GetForestOxygen(quantity float64) (*ForestOxygenProvider, error) {
// 100 magic test number for return error
if quantity >= 100 {
return nil, ErrOxygenSoldOut
}
return &ForestOxygenProvider{
ID: 1,
Price: quantity * 5,
}, nil
}
func GetFirstOxygen(providers []OxygenProvider, quantity float64) (uint64, float64, error) {
for _, provider := range providers {
var response, err = provider(quantity)
if err != nil {
// NOP
// log error
continue
}
return response.GetID(), response.GetPrice(), nil
}
return 0, 0, ErrOxygenSoldOut
}
func TestGetFirstOxygen(t *testing.T) {
{
id, price, err := GetFirstOxygen(nil, 100)
require.Equal(t, uint64(0), id)
require.Equal(t, float64(0), price)
require.True(t, ErrOxygenSoldOut == err)
}
{
id, price, err := GetFirstOxygen([]OxygenProvider{
func(quantity float64) (OxygenProviderResponse, error) {
return GetForestOxygen(quantity)
},
}, 100)
require.Equal(t, uint64(0), id)
require.Equal(t, float64(0), price)
require.True(t, ErrOxygenSoldOut == err)
}
{
id, price, err := GetFirstOxygen([]OxygenProvider{
func(quantity float64) (OxygenProviderResponse, error) {
return GetForestOxygen(quantity)
},
}, 50)
require.Equal(t, uint64(1), id)
require.Equal(t, float64(250), price)
require.True(t, nil == err)
}
}go test ./examples/007-function-casting-err-not-nil/... -v -count=1
=== RUN TestGetFirstOxygen --- PASS: TestGetFirstOxygen (0.00s) PASS
Найважливіше, на що треба звернути увагу, що в Go потрібно явно привести функцію:
var _ func(quantity float64) (*ForestOxygenProvider, error) = GetForestOxygen
// this commented code not compile:
// var _ OxygenProvider = GetForestOxygen
var _ OxygenProvider = func(quantity float64) (OxygenProviderResponse, error) {
return GetForestOxygen(quantity)
}
// this commented code not compile:
// _, _, _ = GetFirstOxygen([]OxygenProvider{GetForestOxygen}, 100)
_, _, _ = GetFirstOxygen([]OxygenProvider{
func(quantity float64) (OxygenProviderResponse, error) {
return GetForestOxygen(quantity)
},
}, 100)Функція GetForestOxygen повертає результат або помилку. Це усна домовленість, якої дотримуються розробники в Go.
А приведення типу до func(quantity float64) (OxygenProviderResponse, error) створює помилку nil is not nil, але код продовжує працювати, бо перша перевірка err != nil.
Якщо перевірку err != nil замінити на response != nil, то помилка стане явною:
package main
// ...
// type OxygenProvider = func(quantity float64) (OxygenProviderResponse, error)
type OxygenProvider = func(quantity float64) OxygenProviderResponse
// ...
func GetForestOxygen(quantity float64) *ForestOxygenProvider {
// 100 magic test number for return error
if quantity >= 100 {
// return nil, ErrOxygenSoldOut
return nil
}
return &ForestOxygenProvider{
ID: 1,
Price: quantity * 5,
}
}
func GetFirstOxygen(providers []OxygenProvider, quantity float64) (uint64, float64, error) {
for _, provider := range providers {
var response = provider(quantity)
if response != nil {
// panic: runtime error: invalid memory address or nil pointer dereference
return response.GetID(), response.GetPrice(), nil
}
}
return 0, 0, ErrOxygenSoldOut
}
func TestGetFirstOxygen(t *testing.T) {
require.Panics(t, func() {
_, _, _ = GetFirstOxygen([]OxygenProvider{
func(quantity float64) OxygenProviderResponse {
return GetForestOxygen(quantity)
},
}, 100)
})
{
id, price, err := GetFirstOxygen([]OxygenProvider{
func(quantity float64) OxygenProviderResponse {
return GetForestOxygen(quantity)
},
}, 50)
require.Equal(t, uint64(1), id)
require.Equal(t, float64(250), price)
require.True(t, nil == err)
}
}
go test ./examples/008-function-casting-response-not-nil/... -v -count=1
=== RUN TestGetFirstOxygen --- PASS: TestGetFirstOxygen (0.00s) PASS
У тесті явно вказав, що очікую паніку, паніка відбулась.
Один з варіантів виправлення:
func GetForestOxygen(quantity float64) OxygenProviderResponse {
// 100 magic test number for return error
if quantity >= 100 {
// return nil, ErrOxygenSoldOut
return nil
}
return &ForestOxygenProvider{
ID: 1,
Price: quantity * 5,
}
}Отже, додаткові обгортки з приведенням типу, є джерелом помилок.
Латання помилок з nil
Правильне рішення — це виправити функцію, щоб повертала справжній nil. Але це правильне рішення потребує часу, тому зустрічаються випадки, коли перевірку на nil переписують з латкою.
Розглянемо можливі латки:
package main
import (
"reflect"
"testing"
"unsafe"
"github.com/stretchr/testify/require"
)
type NameGetter interface {
Name() string
}
type Company struct {
name string
}
func (c *Company) Name() string {
return c.name
}
func TestNotNilBadFixes(t *testing.T) {
var (
company *Company
in NameGetter
)
require.True(t, company == nil)
require.True(t, in == nil)
in = company
require.True(t, in != nil)
// if in != nil
if (*[2]uintptr)(unsafe.Pointer(&in))[1] != 0 {
// do some action
}
require.True(t, (*[2]uintptr)(unsafe.Pointer(&in))[1] == 0)
// if in != nil
if !reflect.ValueOf(in).IsNil() {
// do some action
}
require.True(t, reflect.ValueOf(in).IsNil() == true)
require.Panics(t, func() {
var in interface{}
reflect.ValueOf(in).IsNil()
})
require.Panics(t, func() {
var in struct{}
reflect.ValueOf(in).IsNil()
})
}go test ./examples/009-bad-fixes/... -v
Оскільки reflect.ValueOf(in).IsNil() може панікувати, то також зустрічається такий варіант:
if in != nil && !reflect.ValueOf(in).IsNil() {
// do some action
}func (v Value) IsNil() bool {
k := v.kind()
switch k {
case Chan, Func, Map, Ptr, UnsafePointer:
if v.flag&flagMethod != 0 {
return false
}
ptr := v.ptr
if v.flag&flagIndir != 0 {
ptr = *(*unsafe.Pointer)(ptr)
}
return ptr == nil
case Interface, Slice:
// Both interface and slice are nil if first word is 0.
// Both are always bigger than a word; assume flagIndir.
return *(*unsafe.Pointer)(v.ptr) == nil
}
panic(&ValueError{"reflect.Value.IsNil", v.kind()})
}Такі латки тільки маскують проблему, тому якщо побачили, то треба виправити саме проблему.
На DOU вже була критика книжки «Mastering Go» через її помилки, так само можна знайти статтю Go: Check Nil interface the right way, де замість вирішення помилки, пропонують залатати з reflect.Value.IsNil або навіть додати оператор ===.
Переоцінені можливості nil
Ще одна латка зустрічається, коли можливості Go переоцінюють:
package main
import (
"testing"
"github.com/stretchr/testify/require"
)
type Getter interface {
GetID() int
GetName() string
}
type Company struct {
ID int
Name string
}
func (c *Company) GetID() int {
if c == nil {
return 0
}
return c.ID
}
func (c *Company) GetName() string {
return c.Name
}
type CompanyWrapper struct {
Company
}
type CompanyPointerWrapper struct {
*Company
}
func TestOverestimate(t *testing.T) {
var (
company *Company
companyWrapper *CompanyWrapper
companyPointerWrapper *CompanyPointerWrapper
in Getter = nil
)
require.True(t, in == nil)
in = company
require.True(t, in != nil)
require.True(t, in.GetID() == 0)
require.Panics(t, func() {
in.GetName()
})
// if in != nil
if in != nil && in.GetID() != 0 {
// do some action
}
in = companyWrapper
require.True(t, in != nil)
require.Panics(t, func() {
in.GetID()
})
require.Panics(t, func() {
// if in != nil
if in != nil && in.GetID() != 0 {
// do some action
}
})
in = companyPointerWrapper
require.True(t, in != nil)
require.Panics(t, func() {
in.GetID()
})
require.Panics(t, func() {
// if in != nil
if in != nil && in.GetID() != 0 {
// do some action
}
})
}go test ./examples/010-overestimate/... -v
На LinkedIn є схоже опитування Does this go program panic?, перейдіть та гляньте результати.
package main
import "fmt"
type Source struct {
}
type Wrap struct {
Source
}
func (c *Source) ID() int {
if c == nil {
return 0
}
return 1
}
func main() {
var (
source *Source
wrap *Wrap
)
fmt.Printf("source %d\n", source.ID())
// Does next code panic?
fmt.Printf("wrap %d\n", wrap.ID())
}Епілог
Я переоцінив можливості Go, тому вирішив краще розібратись з nil та написав цю статтю.
Навчальні матеріали
- Francesc Campoy — Understanding nil
- Why Golang Nil Is Not Always Nil? Nil Explained
- 50 відтінків Go по-українськи. Аналізуємо помилки
- gitlab.com/go-yp/go-nil-is-not-nil
Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.

4 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів