首页 > 其他分享 >深入理解nil

深入理解nil

时间:2023-06-16 10:45:03浏览次数:33  
标签:map nil int fmt 理解 深入 func var

nil是Go中熟悉且重要的预先声明的标识符。它是多种类型零值的字面表示。许多具有其他一些流行语言经验的新Go程序员可能会将其nil视为null(或NULL)其他语言的对应物 。这部分是正确的,但nil 在Go和null(或NULL)其他语言之间存在许多差异。

按照Go语言规范,任何类型在未初始化时都对应一个零值:布尔类型是false,整型是0,字符串是"",而指针,函数,interface,slice,channel和map的零值都是nil。

nil 没有默认类型

Go中的每个其他预先声明的标识符都具有默认类型。例如,

  • 默认类型为truefalse 都是bool类型。
  • 默认类型iotaint

但是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

一个为nilslice,除了不能索引外,其他的操作都是可以的,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只是一个标识符,可用于表示某些类型的零值。它不是单一的价值。相反,它可以表示具有不同

存储器布局的许多值。

标签:map,nil,int,fmt,理解,深入,func,var
From: https://www.cnblogs.com/minch/p/17484971.html

相关文章

  • 语义理解技术在情感分析中的应用
    目录情感分析是一种基于自然语言处理技术的人工智能应用,旨在识别文本中的情感和情感倾向。随着人工智能的不断发展,情感分析技术在各个领域得到了广泛的应用,例如客户服务、社交媒体分析、市场调查等。在本文中,我们将探讨语义理解技术在情感分析中的应用,包括基本概念、技术原理、实......
  • 深入理解ASEMI代理光宝LTV-152光耦的特性与应用
    编辑-Z光耦LTV-152是一种广泛应用于电子设备中的光电器件,它的主要功能是实现电路之间的隔离和信号传输。本文将深入探讨光耦LTV-152的特性和应用,帮助读者更好地理解和使用这种重要的电子元件。 一、光耦LTV-152的特性 1.高隔离电压:光耦LTV-152具有高达5000Vrms的隔离电压,......
  • 深入Struts2
    深入Struts2                                                ++YONG原创,转载请注明1.   深入Struts2的配置文件本部分主要介绍struts.xml的常用配置。1.1.   包配置:Struts2框架中核......
  • 《深入探索C++对象模型》- 第一章 - 关于对象 - 笔记
    额外负担:主要由virtual引起1.1C++对象模式简单对象模型(ASimpleObjectModel)Objectptr1->Data1ptr2->Function1ptr3->Function2ptr4->Data2……表格驱动对象模型(ATable-drivenObjectModel)ObjectDatamembertableptrMemberfunctiontab......
  • 个人对DDD(领域驱动设计)理解,以及对实际开发的优化
    目录DDD简单介绍个人理解DDD落地到具体开发的优化总结DDD简单介绍什么是DDD?w我们看看Wiki上怎么说领域驱动设计(英语:Domain-drivendesign,缩写DDD)是一种通过将实现连接到持续进化的模型[1]来满足复杂需求的软件开发方法。领域驱动设计的前提是:把项目的主要重点放在核心领域(c......
  • 深入了解REST约束,优化你的web应用
    REST是RepresentationalStateTransfer的缩写,是RoyFielding于2000年提出的一种通过HTTP设计松散耦合应用程序的架构风格,其主要应用于Web服务的开发。REST不是强制性的,可以不遵守,但它提供了一种高级设计指南。架构约束REST定义了六种架构约束来构成Web服务。RESTfu......
  • TDSQL产品序列理解
    为了增强战斗力和品牌识别度,腾讯把他们的数据库做了一次整合,原来的各种数据库揉在一起成了一个新产品线,分别叫TDSQL-A、TDSQL-C、TDSQL。每个产品是干嘛的?太乱了。谁看得懂这背后到底是几个数据库?作为阿里的同学,把它们大致对标成阿里系的产品,理解起来会简单一些:阿里腾讯分析ADBTD......
  • cgroup cpu 子系统参数深入理解
    shares这个容易理解,无论cpuset.cpus中有多少个cpu,它都是按照比例用。cpu.cfs_quota_us这个就有点坑了,用起来会有些费劲。8核机器,aptasks里有20个线程并行执行,设置cpu.cfs_quota_us为30000(period为100000),预期是cpu能用到30%*8=240%,而实际上只用到了30%。也就是......
  • 关于 mipi cphy 与 dphy 的理解
    客户提出问题:需要cphy的mipi的显示。由于对cphy与dphy不是很了解,查了一些资料。网上的资料。    好了,接下来看看3588核心板原理图。   来说一下自己的理解: mipiD/Cphyport0/1,这个phy, 这个phy有两个port,port是什么意思不知道,对于 ......
  • 深入探讨struts的ActionMessages消息机制
     深入探讨struts的ActionMessages消息机制 版权声明:原创作品,允许转载,转载时请务必以超链接形式标明文章原始出处、作者信息和本声明。否则将追究法律责任。http://leaze.blog.51cto.com/83088/51750   在介绍ActionMessages前,我觉得有必要先介绍一下ActionMessages的数......