• go nil介绍


    go nil介绍

    先从下面的一段代码开始

    下面的代码是模拟了启动多线程去查数据库,之后汇总的操作

    数据库:mysql

    orm框架:ent

    代码:

    package main
    
    import (
    	"awesomeProject/ent"
    	"awesomeProject/ent/user"
    	"context"
    	"fmt"
    	"github.com/go-sql-driver/mysql"
    	"log"
    	"sync"
    )
    
    func main() {
    	config := mysql.Config{
    		User:                 "root",
    		Passwd:               "toor333666",
    		Net:                  "tcp",
    		Addr:                 "localhost:3306",
    		DBName:               "test",
    		AllowNativePasswords: true,
    	}
    
    	client, err := ent.Open("mysql", config.FormatDSN())
    	if err != nil {
    		log.Fatalf("failed opening connection to sqlite: %v", err)
    	}
    	defer client.Close()
    	// 创建表
    	if err := client.Schema.Create(context.Background()); err != nil {
    		log.Fatalf("failed creating schema resources: %v", err)
    	}
    	// 初始化数据
    	ctx := context.Background()
    	for i := 0; i < 10; i++ {
    		for j := 0; j < 3; j++ {
    			client.User.Create().SetName(fmt.Sprintf("name-%d",j)).SetAge(j).SaveX(ctx)
    		}
    	}
    	// 查找数据,比如这里相差各个年龄段的数据,之后做汇总,
    	searchAges := []int{1,2,122}
    	users := make([]interface{}, len(searchAges))
      
    	var waitGroup sync.WaitGroup // 这就是java中的countdownlatch
    	waitGroup.Add(len(searchAges))
    
    	for idx, age := range searchAges {
    		go func(idx,age int) {
    			var ts []*ent.User
    			err := client.User.Query().Where(user.AgeEQ(age)).Select(
    				user.FieldID,user.FieldName,user.FieldAge).Scan(ctx,&ts)
    			if err != nil{ 
    				users[idx] = err
    			}else{
    				users[idx] = ts
    			}
    			waitGroup.Done()
    		}(idx,age)
    	}
    	waitGroup.Wait()
    
    	res := make([]*ent.User, 0)
    	for _, item := range users {
    		if err,ok:= item.(error);ok{
    			panic(err)
    		}
    		if users,ok := item.([]*ent.User);ok{
    			res = append(res, users...)
    		}
    	}
    	println(fmt.Sprintf("%v", res))
    	
    	client.User.Delete().ExecX(ctx)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    go.mod如下:

    module awesomeProject
    
    go 1.18
    
    require (
    	entgo.io/ent v0.11.4
    	github.com/go-sql-driver/mysql v1.6.0
    )
    
    require (
    	ariga.io/atlas v0.7.3-0.20221011160332-3ca609863edd // indirect
    	github.com/agext/levenshtein v1.2.1 // indirect
    	github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
    	github.com/go-openapi/inflect v0.19.0 // indirect
    	github.com/google/go-cmp v0.5.6 // indirect
    	github.com/google/uuid v1.3.0 // indirect
    	github.com/hashicorp/hcl/v2 v2.13.0 // indirect
    	github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
    	github.com/zclconf/go-cty v1.8.0 // indirect
    	golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
    	golang.org/x/text v0.3.7 // indirect
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    代码很简单,有疑问的地方如下如所示:

    在这里插入图片描述
    上面的代码中searchAges参数122是没有数据的,所以结果中必然有一个是没有数据的,但item为nil,断言居然成功了。

    那么,接下来就说说go中nil。

    从感觉上来看,会将它和Java中的null联系起来,但go的nil和其他语言中(Java,Python)的null可不是一回事。

    零值

    go中,变量声明就有默认值,这个值叫做0值,

    • 0 for numeric types,
    • false for the boolean type, and
    • "" (the empty string) for strings.
    • nil for interfaces, slices, channels, maps, pointers and functions.

    nil没有默认的类型

    go中其他的标志符基本都有一个默认的类型比如,

    比如false,true它们的类型是bool类型,iota的类型是int。

    nil没有默认的类型,编译器会从上下文的信息中来推断他的类型,并且在之前

    代码:

    // 下面的代码会编译成功,编辑器可以从上下文中推断出nil的类型
    	_ = (*struct{})(nil) // 这里代码的意思是类型转化,这里能转化不报错,说明类型是正确的。也就是说nil的类型和强转的类型是一样的
    	_ = []int(nil)
    	_ = map[int]bool(nil)
    	_ = chan string(nil)
    	_ = (func())(nil)
    	_ = interface{}(nil)
    
     // 下面的和上面的等价的
    	var _ *struct{} = nil
    	var _ []int = nil
    	var _ map[int]bool = nil
    	var _ chan string = nil
    	var _ func() = nil
    	var _ interface{} = nil
    
     // 编译会失败,信息不足够,编译器也不知道是什么类型。
    	var _ = nil
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    nil在go中不是一个关键词

    nil值在一个上下文中可以被替换掉,代码如下

    	nil := 123
    	fmt.Println(nil) // 123
    
    	// 下面的编译会失败,在当前的上下文中
      // nil已经是一个int的值了,代表的是int类型
    	var _ map[string]int = nil
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    不同类型的空值大小可能不同

    同一类型的所有值的内存布局总是相同的。此类型下的 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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    上面运行的结果不是不变的,编译器和架构会影响。以上打印的结果适用于64位体系结构和标准 Go 编译器。对于32位体系结构,打印大小将减半。

    两个不同类型的nil可能无法比较

    例如,下面示例中的两个比较都无法编译。原因是,在每次比较中,两个操作数都不能隐式转换为另一个操作数的类型。

    go的比较规则可以看这个官网文档

    简而言之。如果其中一个值可以隐式转换为另一个值的类型,则两个值是可比的。

    代码:

    // 编译会失败,类型不匹配
    var _ = (*int)(nil) == (*bool)(nil)         // error
    var _ = (chan int)(nil) == (chan bool)(nil) // error
    
    • 1
    • 2
    • 3

    下面的代码可以编译

    type IntPtr *int
    // 他俩底层的类型都是IntPtr*
    var _ = IntPtr(nil) == (*int)(nil)
    
    // go中每一个interface{}可以表示所有
    var _ = (interface{})(nil) == (*int)(nil)
    
    // 当向通道可以转换为单向通道,并且他俩底层的数据类型一致
    var _ = (chan int)(nil) == (chan<- int)(nil)
    var _ = (chan int)(nil) == (<-chan int)(nil)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    两个相同类型的 Nil 值可能无法比较

    在 Go 中,映射、切片和函数类型不支持比较。比较无法比较的类型的两个值(包括空值)是非法的。 这是语法限制,其实和nil值感觉不是很大。

    下列比较无法编译。

    var _ = ([]int)(nil) == ([]int)(nil)
    var _ = (map[string]int)(nil) == (map[string]int)(nil)
    var _ = (func())(nil) == (func())(nil)
    
    • 1
    • 2
    • 3

    下面是ok的

    	var _ = ([]int)(nil) == nil
    	var _ = (map[string]int)(nil) == nil
    	var _ = (func())(nil) == nil
    
    • 1
    • 2
    • 3

    两个nil值可能不相等

    如果比较的nil中有一个是接口值,结果总等于false,因为他们的底层的类型不一致,编译器不能推断出来,我们知道go的接口的底层有一对数据(类型和实际的值。)对于下面的例子中

    interface{}(nil) 的一对数据为(type=nil,value=nil)

    (*int)(nil) )的一对数据为 (type=*int,value=nil)

    fmt.Println( (interface{})(nil) == (*int)(nil) ) // false
    
    • 1

    从nil值中检索数据不会Panic

    从空映射值中检索元素将始终返回零元素值。

    fmt.Println( (map[string]int)(nil)["key"] ) // 0
    fmt.Println( (map[int]bool)(nil)[123] )     // false
    fmt.Println( (map[int]*int64)(nil)[123] )   // 
    
    • 1
    • 2
    • 3

    从nil的Channels、Maps、Slices、数组指针遍历是合法的

    遍历map和slices类型的nil的步长为0

    遍历数组指针的步长是它对应的数组的长度

    遍历一个nil的channel会堵塞当前线程

    代码:

    for range []int(nil) {
    	fmt.Println("Hello")
    } // 不会输出
    
    
    for range map[string]string(nil) {
    	fmt.Println("world")
    } // 不会出输出
    
    for i := range (*[5]int)(nil) {
    	fmt.Println(i)
    } // 输出0,1,2,3,4
    
    
    for range chan bool(nil) { // 堵塞在这里,被死锁检测到。
    	fmt.Println("Bye")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    通过非接口的nil值来调用方法不会panic

    代码:

    package main
    
    type Slice []bool
    
    func (s Slice) Length() int {
    	return len(s)
    }
    
    func (s Slice) Modify(i int, x bool) {
    	s[i] = x // panic if s is nil
    }
    
    func (p *Slice) DoNothing() {
    }
    
    func (p *Slice) Append(x bool) {
    	*p = append(*p, x) // panic if p is nil
    }
    
    func main() {
     // 下面的不会抛错
    	_ = ((Slice)(nil)).Length
    	_ = ((Slice)(nil)).Modify
    	_ = ((*Slice)(nil)).DoNothing
    	_ = ((*Slice)(nil)).Append
    
    
    	_ = ((Slice)(nil)).Length()
    	((*Slice)(nil)).DoNothing()
    
    	// 下面的会报错,但是报错不是在调用方法的时候,而是在方法里面操作对象的时候报错
    	/*
    	((Slice)(nil)).Modify(0, true)
    	((*Slice)(nil)).Append(true)
    	*/
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    *new(T)的结果就是T类型下面的0值

    package main
    
    import "fmt"
    
    func main() {
    	fmt.Println(*new(*int) == nil)         // true
    	fmt.Println(*new([]int) == nil)        // true
    	fmt.Println(*new(map[int]bool) == nil) // true
    	fmt.Println(*new(chan string) == nil)  // true
    	fmt.Println(*new(func()) == nil)       // true
    	fmt.Println(*new(interface{}) == nil)  // true
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    总结

    在 Go 中,为了简单和方便,nil 被设计为一个标识符,可以用来表示某些类型的零值。他不是一个不变的值,而是随着不同类型有不同的类型,在日常的编码中,编译器会根据上下文来推导出。

    回答代码中的问题

    首先代码中出现问题的是interface{},按照上面说的「两个nil值可能不相等」来说,结果已经推出来了。在go中接口是一堆方法的集合,并且接口变量的底层是一对数据结构(类型,实际的值),断言,反射是来拿到它们在做处理,先看下面的代码

    var p *int              // (type=*int,value=nil)
    var i interface{}       // (type=nil,value=nil)
    
    if i != p {             // (type=*int,value=nil) != (type=nil,value=nil)
    // to successfully compare these values, both type and value must match
        fmt.Println("not a nil")
    }
    // 运行结果是   "not a nil"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这两虽然都是nil,但他们的类型不一致。

    在看一段代码

    var p *int              // (type=*int,value=nil)
    	var i interface{}       // (type=nil,value=nil)
    
    	if p != nil{ // (type=*int,value=nil) != (type=*int,value=nil)
    		fmt.Println("not a nil p")
    	}
    
    
    	if i != nil {           // (type=nil,value=nil) != (type=nil,value=nil)
    		fmt.Println("not a nil i1")
    	}
    
    	i = p                   // assign p to i
    
    	// a hardcoded nil is always nil,nil (type,value)
    	if i != nil {           // (type=*int,value=nil) != (type=nil,value=nil)
    		fmt.Println("not a nil i2")
    	}
    // 运行结果是   "not a nil i"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    类比一开始的代码,结合上面的分析,看下面的解释。

    在这里插入图片描述### 如何处理这种情况

    有三种方式,代码如下

    import "reflect"
    import "unsafe"
    
    func do(v interface{}){
        // 可能有问题,因为这是接口类型的变量
        // 比如传递进来的是 (type=*int,value=nil),永远不会为true,这里的nil是 (type=nil,value=nil)
        if v == nil {
    
        }
    		
       // 这种方式是ok的,将nil值转换为同一类型下来比较
       // 但是这样的比较要对业务很熟悉,实际动起手来比较麻烦
        if v == (*int)(nil){
          
        }
    		
      // 因为是nil,所以可以拿到 反射中的Value是否为nil
        if reflect.ValueOf(v).IsNil() {
        
        }
    	// 通过unsafe来直接检查一个接口类型的变量的value部分是否为0,但是如果value不是可以为nil的那5中类型的话也不会报错
        if (*[2]uintptr)(unsafe.Pointer(&v))[1] == 0 {
       
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    对于上面的代码,可以采取下面的方式来判断

    在这里插入图片描述

    相关博客:

    Why Golang Nil Is Not Always Nil? Nil Explained

    nils in Go


    关于博客这件事,我是把它当做我的笔记,里面有很多的内容反映了我思考的过程,因为思维有限,不免有些内容有出入,如果有问题,欢迎指出。一同探讨。谢谢。
  • 相关阅读:
    【Python百日进阶-Web开发-Feffery】Day400 -“一起Dash”训练营Lesson-09_利用多页面应用_课堂
    Java并发-多线程售票案例
    盘点10个地推拉新和网推拉新app推广接单平台,免费一手渠道平台
    漏洞修复:在应用程序中发现不必要的 Http 响应头
    编辑器的缩略图实现原理
    [超详细]SpringBoot整合WebSocket
    【python】(七)python内置装饰器: @classmethod和@staticmethod
    “人生苦短,我用Python“——身份认证攻击
    Vue安装过程的困惑解答——nodejs和vue关系、webpack、vue-cli、vue的项目结构
    harmony 鸿蒙使用N-API开发Native模块
  • 原文地址:https://blog.csdn.net/daliucheng/article/details/127761672