Нулевое значение Golang, нулевое значение и пустая структура

Go
Нулевое значение Golang, нулевое значение и пустая структура

Эта статья была впервые опубликована вличный блог at7h

В этой статье мы обсудим вопросы, связанные с нулевым значением, nil и пустой структурой в Golang, а также некоторые из их применений.

нулевое значение

Нулевое значение относится к стратегии всегда автоматически устанавливать начальное значение по умолчанию для вашей переменной, когда вы объявляете ее (выделяете память) без явной ее инициализации.

Во-первых, давайте посмотрим на официальное нулевое значение (The zero value) Технические характеристики:

When storage is allocated for a variable, either through a declaration or a call of new, or when a new value is created, either through a composite literal or a call of make, and no explicit initialization is provided, the variable or value is given a default value. Each element of such a variable or value is set to the zero value for its type: false for booleans, 0 for numeric types, "" for strings, and nil for pointers, functions, interfaces, slices, channels, and maps. This initialization is done recursively, so for instance each element of an array of structs will have its fields zeroed if no value is specified.

Из этого мы можем сделать вывод:

  • затип значения: Логический типfalse, тип значения0, строка"", массивы и структуры рекурсивно инициализируют свои элементы или поля, т.е. их начальные значения зависят от элементов или полей.
  • затип ссылки: обеnil, включая указатель указателя, функциональную функцию, интерфейс интерфейса, срез среза, канал канала, карту карты.

Часто бывает полезно присвоить значение по умолчанию объявленным вами переменным, особенно для элементов или полей в ваших массивах и структурах.Это безопасная и правильная практика, а также позволяющая сохранить ваш код кратким.

Например, обычно используется следующий пример, структураValueсодержит два неэкспортированных поля,sync.MutexТакже вдва неэкспортированных поля. Поскольку существует нулевое значение по умолчанию, мы можем использовать его напрямую:

package main

import "sync"

type Value struct {
    mu sync.Mutex
    val int
}

func (v *Value)Incr(){
    defer v.mu.Unlock()

    v.mu.Lock()
    v.val++
}

func main() {
    var i Value

    i.Incr()
}

Поскольку срез является ссылочным типом, его нулевое значение такжеnil:

package main

import "fmt"
import "strings"

func main(){
    var s []string

    fmt.Println(s, len(s), cap(s)) // [] 0 0
    fmt.Println(s == nil) // true

    s = append(s, "Hello")
    s = append(s, "World")
    fmt.Println(strings.Join(s, ", ")) // Hello, World
}

Особого внимания требуют следующие ситуации: иногда легко запутаться, если не обращать внимания.:=Синтаксический сахар этообъявить и инициализировать переменные, так это реальный экземпляр (для которого был назначен адрес памяти), а не нулевойnil:

package main

import "fmt"
import "reflect"

func main() {
    var s1 []string
    s2 := []string{} // 或者等同于 var s2 = []string{}

    fmt.Println(s1 == nil) // true
    fmt.Println(s2 == nil) // false

    fmt.Println(reflect.DeepEqual(s1, s2)) // false

    fmt.Println(reflect.DeepEqual(s1, []string{}))  // false
    fmt.Println(reflect.DeepEqual(s2, []string{}))  // true
}

Также для пустых структурnilМожно вызывать методы этого типа, которые также можно использовать для простого предоставления значений по умолчанию:

package main

import "fmt"

const defaultPath = "/usr/bin/"

type Config struct {
    path string
}

func (c *Config) Path() string {
    if c == nil {
            return defaultPath
    }
    return c.path
}

func main() {
    var c1 *Config
    var c2 = &Config{
            path: "/usr/local/bin/",
    }
    fmt.Println(c1.Path(), c2.Path())
}

nil

Для разработчика, который только начинает работать с Golang, только начинаетnilЕго следует использовать для проверки ошибок, примерно так:

func doSomething() error {
    return nil
}

func main(){
    if doSomething() != nil {
        return err
    }
}

Это идиоматично в Golang и побуждает разработчиков явно обрабатывать ошибки как возвращаемые значения. Теперь давайте обсудим этоnil, аналогичные определения есть и в других языках, таких как C, C++, Java и т. д.null, на ПитонеNone, но в GoalngnilС ними много различий.

nil— это предварительно объявленный идентификатор (зарезервированное слово, не являющееся ключевым словом) в Golang, который в основном используется для представления нулевого значения ссылочных типов (указателей, интерфейсов, функций, карт, срезов и каналов), представляющих их неинициализированные значения.

// [src/builtin/builtin.go](https://golang.org/src/builtin/builtin.go#L98)
//
// nil is a predeclared identifier representing the zero value for a
// pointer, channel, func, interface, map, or slice type.
var nil Type // Type must be a pointer, channel, func, interface, map, or slice type

nilэто единственное нетипизированное значение в Golang, у которого нет типа по умолчанию, это не неопределенное состояние. Таким образом, вы не можете использовать его следующим образом:

a := nil
// cannot declare variable as untyped nil: a

не будет иметь типаnilзначение, присвоенноеaнеправильно, компилятор не знает, что он должен датьaКакой тип назначить.

Стоит отметить, что самые известные в Голангеnil != nilПроблема, давайте рассмотрим пример ниже:

var p *int
var i interface{}

fmt.Println(p)      // <nil>
fmt.Println(i)      // <nil>

fmt.Println(p == i) // false

Почему? почему жеnilНо не равно?

С вопросом давайте рассмотрим следующий пример (из официальногоWhy is my nil error value not equal to nil):

func Foo() error {
    var err *MyError = nil
    if bad() {
        err = ErrBad
    }
    return err
}

func main() {
    err := Foo()
    fmt.Println(err)        // <nil>
    fmt.Println(err == nil) // false
}

Виновникinterface, принцип реализации, относящийся к интерфейсу, выходит за рамки этой статьи и будет рассмотрен позже. Общий принцип заключается в том, что для определения переменной интерфейсу нужны два основных свойства:Type and Value, давайте добавим комментарий к двум приведенным выше фрагментам кода, чтобы понять:

var p *int          // (T=*int,V=nil)
var i interface{}   // (T=nil,V=nil)

fmt.Println(p == i) // (T=*int, V=nil) == (T=nil, V=nil) -> false
func Foo() error {
    var err *PathError = nil  // (T=*PathError, V=nil)
    if bad() {
        err = ErrBad
    }
    return err  // 这将始终返回 non-nil 错误
}

func main() {
    err := Foo()
    fmt.Println(err)        // <nil>
    fmt.Println(err == nil) // (T=*PathError, V=nil) == (T=nil, V=nil) -> false
}

ПОЖАЛУЙСТА, ОБРАТИТЕ ВНИМАНИЕ: чтобы избежать этой проблемы, всегда используйте при возврате ошибкиerrorинтерфейса и никогда не инициализируйте пустую переменную ошибки, которая может быть возвращена функцией.

Давайте изменим приведенный выше пример и посмотрим на следующий пример:

var p *int              // (T=*int, V=nil)
var i interface{}       // (T=nil, V=nil)

fmt.Println(p == nil)   // true
fmt.Println(i == nil)   // true

i = p

fmt.Println(i == nil)     // (T=*int, V=nil) == (T=nil, V=nil) -> false

Суть этой проблемы в том, что в Go TourInterface values with nil underlying values.

примерiможет быть передан вinterface{}в качестве входного параметра, вы просто проверяетеi == nilнедостаточно. Так что для суждения о нулевом указателе типа интерфейса иногда на него нельзя смело полагатьсяv == nil, хотя эта контрольная яма случается редко, иногда она может привести к сбою вашей программы. Есть два способа решить эту проблему, вы можете разделить тип и значение с помощьюnilСравните или используйте пакет отраженияreflect.

Помните: если в интерфейсе хранятся какие-либо конкретные значения, интерфейс не будетnil, видетьзакон отражения.

Есть, может быть, вы тоже запутались, еще приведенный выше пример, почему следующие типы можно сравнивать напрямую и получать точные результаты:

var p *int              // (T=*int, V=nil)

fmt.Println(p == nil)   // true

Это потому, что при выполнении приведенного выше сравнения компилятор уже знаетpтип, поэтому компилятор может преобразовать вp == (*int)(nil). Но для интерфейсов компилятор не может определить базовый тип, поскольку его можно изменить.

пустая структура

Пустая структура — это тип структуры без каких-либо полей, например:

type Q struct{}
var q struct{}

Какая польза от него, если у него нет полей?

Мы знаем, что размер экземпляра структуры (то есть количество байт занимаемой памяти) определяется шириной (размером) и выравниванием (выравниванием) ее полей, что помогает скорости адресации, языка С, и т. д. Существуют похожие стратегии, пожалуйста, прочитайте о конкретных стратегиях Golang.Size and alignment guarantees.

Ясно, что размер пустой структуры равен нулю байтов:

var q struct {}
fmt.Println(unsafe.Sizeof(q)) // 0

Поскольку пустые структуры занимают ноль байтов, выравнивание заполнения не требуется, поэтому пустые структуры с вложенными пустыми структурами также не занимают место в памяти.

type Q struct {
        A struct{}
        B struct{
            C struct{}
        }
}
var q Q
fmt.Println(unsafe.Sizeof(q)) // 0

Поскольку пустые структуры не занимают места в памяти, мы объявляем массивы или слайсы с пустыми структурами как элементы, которые также не занимают места (Orthogonality in Go):

var x [1000000000]struct{}
fmt.Println(unsafe.Sizeof(x)) // 0

var y = make([]struct{},1000000000)
fmt.Println(unsafe.Sizeof(x))// 24,背后关联数组为 0

Для пустой структуры (или пустого массива) размер хранилища, который она занимает, равен нулю, поэтому в памяти находятся две разные переменные нулевого размера.может иметь тот же адрес.

Взгляните на следующие примеры:

var a, b struct{}
fmt.Println(&a == &b)  // true


c := make([]struct{}, 10)
d := make([]struct{}, 20)
fmt.Println(&c[0] == &d[1]) // true


type Q struct{}

func (q *Q)addr() { fmt.Printf("%p\n", q) }

func main() {
        var a, b Q
        a.addr()  // 0x5af5a60
        b.addr()  // 0x5af5a60
}

e := struct{}{} // 不是零值,一个真正的实例
f := struct{}{}
fmt.Println(e == f) // true

Обратите внимание, что это равенство только возможно, но не достоверно.

такПример, см. это для объяснения связанных вопросовissue.

После долгого разговора вы можете подумать, что это, кажется, не имеет практического применения.Вот еще два практических практического применения:

1. использоватьchan struct{}заменятьchan boolПередача сигналов между горутинами. использоватьboolЛюдям легко не понять ценности,true or false, но используяchan struct{}Понятно, что нас не волнует значение, нас волнует только то, что произошло, и это проще выразить ясно.

2.Чтобы предотвратить инициализацию структуры без ключа, вы можете добавить_ struct {}Поле:

type Q struct {
  X, Y int
  _    struct{}

Таким образом, используйтеQ{X: 1, Y: 1}Да, но использоватьQ{1, 1}Возникает ошибка компиляции:too few values in struct initializer, что тоже помоглоgo verПроверка кода.

Ссылаться на


Одноименный публичный аккаунт: