内容简介:按照Go语言规范,任何类型在未初始化时都对应一个零值:布尔类型是false,整型是0,字符串是"",而指针,函数,interface,slice,channel和map的零值都是nil。Go中的每个其他预先声明的标识符都具有默认类型。例如,
9.2 深入理解nil
nil
是 Go 中熟悉且重要的预先声明的标识符。它是多种类型零值的字面表示。许多具有其他一些流行语言经验的新Go程序员可能会将其 nil
视为 null
(或 NULL
)其他语言的对应物 。这部分是正确的,但 nil
在Go和 null
(或 NULL
)其他语言之间存在许多差异。
按照Go语言规范,任何类型在未初始化时都对应一个零值:布尔类型是false,整型是0,字符串是"",而指针,函数,interface,slice,channel和map的零值都是nil。
nil
没有默认类型
Go中的每个其他预先声明的标识符都具有默认类型。例如,
- 默认类型为
true
和false
都是bool
类型。 - 默认类型
iota
是int
。
但是 nil
它没有默认类型,尽管它有许多可能的类型。编译器必须有足够的信息来 nil
从上下文中推导出值的类型 。
示例:
package main func main() { // This following line doesn't compile. /* v := nil */ // There must be sufficient information for compiler // to deduce the type of a nil value. _ = (*struct{})(nil) _ = []int(nil) _ = map[int]bool(nil) _ = chan string(nil) _ = (func())(nil) _ = interface{}(nil) // This lines are equivalent to the above lines. var _ *struct{} = nil var _ []int = nil var _ map[int]bool = nil var _ chan string = nil var _ func() = nil var _ interface{} = nil }
nil Go中是一个预先声明的标识符
您可以在 nil
不声明的情况下使用它。
nil
可以表示多种类型的零值
在Go中, nil
可以表示以下类型的零值:
- pointer types (including type-unsafe ones).
- map types.
- slice types.
- function types.
- channel types.
- interface types.
示例:
package main import "fmt" type Person struct { Id int Name string Info interface{} } func main() { var p Person fmt.Println(p)// {0 <nil>} }
nil
在Go中不是关键字
预先宣布的 nil
可以被遮蔽。
示例:
package main import "fmt" func main() { nil := 123 fmt.Println(nil) // 123 }
nil
具有不同种类的价值的大小可能不同
一个类型的所有值的内存布局总是相同的。 nil
类型的值不是例外。 nil
值的大小始终与其类型与 nil
值相同的非零值的大小相同。因此, nil
表示不同类型的不同零值的标识符可以具有不同的大小。
示例:
package main import ( "fmt" "unsafe" ) func main() { var p *struct{} = nil fmt.Println( unsafe.Sizeof( p ) ) // 8 var s []int = nil fmt.Println( unsafe.Sizeof( s ) ) // 24 var m map[int]bool = nil fmt.Println( unsafe.Sizeof( m ) ) // 8 var c chan string = nil fmt.Println( unsafe.Sizeof( c ) ) // 8 var f func() = nil fmt.Println( unsafe.Sizeof( f ) ) // 8 var i interface{} = nil fmt.Println( unsafe.Sizeof( i ) ) // 16 }
nil 使用场景
pointers
nil pointer
- 指向 nil, 又名 nothing
- pointer 的零值
var p *int // 声明一个 int 类型的指针 println(p) // <nil> p == nil // true *p // panic: runtime error: invalid memory address or nil pointer dereference
指针表示指向内存的地址,如果对 nil 的指针进行解引用的话就会导致 panic。那么为 nil 的指针有什么用呢? 先来看看一个计算二叉树和的例子:
type tree struct { v int l *tree r *tree } // first solution func (t *tree) Sum() int { sum := t.v if t.l != nil { sum += t.l.Sum() } if t.r != nil { sum += t.r.Sum() } return sum }
上面代码有两个问题:
-
一个是代码重复
if v != nil { v.m() }
另一个是当 t 是 nil 的时候会 panic:
var t *tree sum := t.Sum() // panic: invalid memory address or nil pointer dereference
那,怎么解决上面的问题呢? 我们先来看看一个指针接收器的例子:
type Person struct{} func sayHi(p *Person) {fmt.Println("hi")} func (p *Person) sayHi() {fmt.Println("hi")} var p *Person p.sayHi() // hi
对于指针对象的方法来说,就算指针的值为 nil, 也是可以调用的,基于此,我们可以对刚刚计算的二叉树的例子进行一下改造:
func (t *tree) Sum() int { if t == nil { return 0 } return t.v + t.l.Sum() + t.r.Sum() }
跟刚才的代码一对比是不是简洁了很多? 对于 nil
指针,只需要在方法前面判断一下就 OK 了,无需重复判断。换成打印二叉树的值或者查找二叉树的某个值都是一样的:Coding Time
func(t *tree) String() string { if t == nil { return "" } return fmt.Sprint(t.l, t.v, t.r) } // nil receivers are useful: Find func (t *tree) Find(v int) bool { if t == nil { return false } return t.v == v || t.l.Find(v) || t.r.Find(v) }
所以如果不是很需要的话,不要用NewX()去初始化值,而是使用它们的默认值。
slices
// nil slices var s []T len(s) // 0 cap(s) // 0 for range s { } // iterates zero times s[i] // panic: index out of range
一个为 nil
的 slice
,除了不能索引外,其他的操作都是可以的, slice
有三个元素,分别是长度、容量、指向数组的指针,当你需要填充值的时候可以使用 append
函数, slice
会自动进行扩充。所以我们并不需要担心 slice
的大小,使用 append
的话 slice
会自动扩容。
var s []int for i := 0; i < 10; i++ { fmt.Printf("len: %2d cap: %2d\n", len(s), cap(s)) s = append(s, i) }
那么为 nil
的slice的底层结构是怎样的呢?根据官方的文档,slice有三个元素,分别是长度、容量、指向数组的指针:
map
对于 Go 来说,map,function, channel 都是特殊的指针,指向各自特定的实现,这个我们暂时可以不用管。
// nil maps var m map[t]u len(m) // 0 for range m // interates zero times v, ok := m[i] // zero(u), false m[i] = x // panic: assignment to entry in nil map
对于 nil
的 map
, 我们可以简单把它看成是一个只读的 map,不能进行写操作,否则就会 panic。那么, nil
的 map 有什么用呢? 看下这个例子:
func NewGet(url string, headers map[string]string) (*http.Request, error) { req, er := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, err } for k, v := range headers { req.Header.Set(k, v) } return req, nil }
对于 NewGet 来说,我们需要传入一个类型为 map 的参数,并且这个函数只是对这个参数进行读取,我们可以传入一个非空的值:
NewGet("http://google.com", map[string]string) { "USER_AGENT":"golang/gopher", } // 或者,这样传 NewGet("http://google.com", map[string]string{}) // 但是,前面也说了,map 的零值是 nil, 所以当 header 为空的时候,我们也可以直接传入一个 nil NewGet("http://google.com", nil)
是不是,简洁很多? so, 把 nil
map 作为一个只读的空的 map 进行读取吧
channels
// nil channels var c chan t <- c // blocks forever c <- x // blocks forever close(c) // panic: close of nil channel
关闭一个 nil
的 channel 会导致程序 panic (如何关闭 channel 可以看看这篇文章: 如何优雅的关闭Go Channel ). 举个例子,假如现在有两个 channel 负责输入,一个 channel 负责汇总,简单的代码实现:
func merge(out chan<- int, a, b <-chan int) { for { select { case v := <- a: out <- v case v := <- b: out <- v } } }
closed channels
var c chan t v, ok <- c // zero(t), false c <- x // panic: send on closed channel close(c) // panic: close of nil channel
如果在外部调用中关闭了 a 或者 b, 那么就会不断地从 a 或者 b 中读出 0,这和我们想要的不一样,我们想关闭 a 或 b 后就停止汇总,修改一下代码:
func merge (out chan<- int, a, b <-chan int) { for a != nil || b != nil { select { case v, ok := <-a: if !ok { a = nil fmt.Println("a is nil") continue } out <- v case v, ok := <-b: if !ok { a = nil fmt.Println("b is nil") continue } out <- v } } fmt.Println("close out") close(out) }
在知道 channel 关闭之后,将 channel 的值设为 nil, 这样子就相当于将这个 select case 子句给停用了,因为 nil
的 channel 是永远阻塞的。
functions
函数可以被用作结构体字段, 逻辑上,默认的零值为 nil
type Foo struct { f func() error }
nil funcs for default values
lazy initialization of variables, nil can also imply default behavior
func NewServer(logger func(string, ...interface{})) { if logger == nil { logger = logger.Printf } logger("initializing %s", os.Getenv("hostname")) // ... }
interfaces
interface 并不是一个指针,它的底层实现由两部分组成,一个是类型,一个是值,也就类似于:(Type, Value). 只有当类型和值都是 nil 的时候,才等于 nil. 看看下面的代码:
func do() error { // error: (*doError, nil) var err *doError return err // nil of type *doError } func main() { err := do() fmt.Println(err == nil) // false }
输出结果:false. do 函数声明了一个 *doError 的变量 err, 然后返回,返回值是 error 接口,但是这个时候的 Type 已经变成了:(*doError, nil), 所以和 nil 肯定是不会相等的。所以我们在写函数的时候,不要声明具体的 error 变量,而是应该直接返回 nil:
func do() error { return nil } // 再来看看这个例子 func do() *doError { // nil of type *doError return nil } func wrapDo() error { // error (*doError, nil) return do() // nil of type *doError } func main() { err := wrapDo() // error (*doError, nil) fmt.Println(err == nil) // false }
这里最终的输出结果也是 false。 为什么呢? 尽管 wrapDo 函数返回的是 error 类型, 但是 do 返回的却是 *doError 类型,也就是变成了 (*doError, nil), 自然也就和 nil 不相等了。因此,不要返回具体的错误类型。遵从这两条建议,才可以放心的使用 if x != nil
.
在Go中, nil
只是一个标识符,可用于表示某些类型的零值。它不是单一的价值。相反,它可以表示具有不同
存储器布局的许多值。
links
- 目录
- 上一节:
- 下一节:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 【1】JavaScript 基础深入——数据类型深入理解与总结
- 深入理解java虚拟机(1) -- 理解HotSpot内存区域
- 深入理解 HTTPS
- 深入理解 HTTPS
- 深入理解 SecurityConfigurer
- 深入理解 HTTP 协议
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。