Переоцінені можливості nil в Go або Nil is not nil

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Привіт! У цій статті розглянемо помилку перевірки на 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]

play.golang.org/p/rQ-tzW2ZLai

Схожий приклад «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 (
	"github.com/stretchr/testify/require"
	"testing"
)

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 (
	"github.com/stretchr/testify/require"
	"testing"
)

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 (
	"github.com/stretchr/testify/require"
	"testing"
)

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"
	"github.com/stretchr/testify/require"
	"testing"
)

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 (
	"github.com/stretchr/testify/require"
	"reflect"
	"testing"
	"unsafe"
)

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 (
	"github.com/stretchr/testify/require"
	"testing"
)

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 та написав цю статтю.

Навчальні матеріали

👍НравитсяПонравилось6
В избранноеВ избранном3
LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Добре пояснили що nil struct != nil interface, оскільки інтерфейс всередині має ще й додатково лінк на тип.

Очень толковый материал, спасибо !

Дуже важко читати. В прикладах немає місць на які треба звернути увагу.
Було б непогано додати більше коментів по справі у приклади, або зробити якійсь хайлайти з текстом, на що саме треба звертати увагу.
Буди компілятором упродовж читання статті не хочеться.
За старання звісно подяка, але все дуже сухо, прямо як у ГоФів.

Ось простіше пояснення помилки «nil» Interfaces and «nil» Interfaces Values.

Ось простіше пояснення nil Francesc Campoy — Understanding nil.

В цій статті описував помилку яка трапилась в реальному проєктів, код в проєкті звісно ж складніше ніж в статтях які навів в навчальних матеріалах.

Подписаться на комментарии