Go中必须谈论的四个迷点

c/c++

浏览数:131

2020-6-15

AD:资源代下载服务

很多熟悉Go的程序员们都会说到Go是一门很简单的语言,话虽如此,但实际上Go的简单是基于复杂底层的极简包装。

Go在很多地方均做了“隐式”的转换,这也就导致了很多迷惑点,本文总结了Go开发中几个令人迷惑的地方,如有不当之处请指正。

nil究竟是什么

首先明确一点:nil是值而非类型。nil值只能赋值给slice、map、chan、interface和指针。

在Go中,任何类型都会有一个初始值。数值类型的初始值为0,slice、map、chan、interface和指针类型的初始值为nil,对于nil值的变量,我们可以简化理解为初始状态变量。

但nil在实际使用过程中,仍有不少令人迷惑的地方。

var err error
e := &err
if e != nil {
    fmt.Printf("&err is not nil:%p\n", e)
}
// 输出:&err is not nil:0xc0000301f0

err是一个接口类型的变量,其初始值为nil,然后对err进行取址操作会发现能成功取到地址,这就是Go和C++最大的不同之一。有C++基础的人在刚接触Go的时候,自然而然的会认为nil是个空指针类型值,上面的代码力证在Go中,nil只是一个表示初始状态的值

对于slicemapchaninterface,当值为nil时,不具备可写性。

// 1
var s []int
fmt.Printf("%v\n", s[0])
// 输出panic

// 2
var c chan int
val := <-c
fmt.Printf("%v\n", val)
// 输出panic

// 3
var m map[int]int
m[1] = 123
// 输出panic

上面3段代码均会出现panic,对于slicemapchan类型的nil值变量,可以理解为可读不可写,只有通过make(new)创建的对象实例满足可写性。

接口的本质

Go官方文档中表示:interface本身是引用类型,即接口类型本身是指针类型。

type Animal interface {
    Barking()
}

type Cat struct {
}

func (c *Cat) Barking() {
    fmt.Printf("Meow~~\n")
}

type Dog struct{}

func (d Dog) Barking() {
    fmt.Printf("W~W~W~\n")
}

Cat和Dog类型都实现了Barking接口,需要注意的是,Cat是以指针接收器方式实现Barking接口,Dog是以值传递方式实现Barking接口。在Go中,当调用接口方法时,会自动对指针进行解引用。下面的代码可以证明这一点:

d := &Dog{}
d.Barking()

c := Cat{}
c.Barking()
/* 输出:
 W~W~W~
 Meow~~
*/

接口的作为函数参数如何传递?

func AnimalBarking(a Animal) {
    a.Barking()
}

根据上面这段代码,如何调用AnimalBarking方法呢?
首先明确Animal是引用类型(指针),由于接口会自动对传递的指针进行解引用,所以当接口类型作为函数参数传递时,有以下规则:

  • 当以指针接收器实现接口方法时,传递AnimalBarking的参数必须为对象指针。
  • 当以对象接收器实现接口方法时,传递AnimalBarking的参数既可以是对象指针(指针会自动解引用),也可以是对象实例。

下面的代码合法:

d1 := &Dog{}
AnimalBarking(d1)

d2 := Dog{}
AnimalBarking(d2)

很多地方都专门指出:指向接口的指针是无意义的。事实是否真的是这样子?

我们知道在Go中有专门的接口类型,接口类型在runtime中大概是这样:

type iface struct {
    tab  *itab  // 8bytes
    data unsafe.Pointer // 8bytes
}

对于接口而言,可能会存在这样的代码:

type Handler interface {
    Func()
}

type Server struct{}

func (s *Server) Func() {
    fmt.Printf("*Server.Func\n")
}

func Func(handler *Handler) {
    handler.Func()
}

上面的代码在Go1.13下无法通过编译:handler.Func undefined (type *Handler is pointer to interface, not interface)
这里要清楚,指向结构的指针和指向接口的指针是两回事,接口直接存放了结构的类型信息以及结构指针。在Go中,无法为实现了接口方法的struct生成指向接口的指针并调用接口方法。

但当修改为如下代码则可编译通过:

func Func(handler *Handler) {
    if handler != nil && (*handler) != nil {
        (*handler).Func()
    }
}

从Go设计角度出发的。Go里面的interface是个结构,保存了类型信息和数据拷贝,所谓的无意义我更多是认为Go设计上的理念,因为nil值的存在且又是指针,那么问题就回落到有无办法一步判断指针和值是否合法。显然这种代码有违Go的设计初衷,但指向接口的指针这种东西确实存在于Go中,对此的解释在这里:https://golang.org/doc/faq#po…

【指向接口的指针是无意义的】这句话确实有点绝对,但确实是存在且意义并不大的东西。

关于接口的延申阅读:Go interface

defer机制

在Go中提供defer这样优雅的函数退出后“收尾”操作,但很多人会忽略defer机制中的一点:defer在声明时引用到的变量就已被实时编译。下面的代码:

var ErrNotFound error = errors.New("Not found")
func TestDefer1() error {
    var err error
    defer fmt.Printf("TestDefer1 err: %v\n", err)

    // ...
    err = ErrNotFound
    return err
}

/* 输出:
 TestDefer1 err: <nil>
*/

当defer声明func时,情况不一样了:

func TestDefer2() error {
    var err error
    defer func() {
        fmt.Printf("TestDefer2 err: %v\n", err)
    }()
    // ...
    err = ErrNotFound
    return err
}

/* 输出:
 TestDefer2 err: Not found
*/

所以:defer在声明语句时引用到的变量就已被实时编译

读写chan是否应该加锁

先说答案:不需要。具体原因可以从runtime/chan.go中知道。chan的原始struct如下:

type hchan struct {
    qcount   uint           // total data in the queue
    dataqsiz uint           // size of the circular queue
    buf      unsafe.Pointer // points to an array of dataqsiz elements
    elemsize uint16
    closed   uint32
    elemtype *_type // element type
    sendx    uint   // send index
    recvx    uint   // receive index
    recvq    waitq  // list of recv waiters
    sendq    waitq  // list of send waiters

    // lock protects all fields in hchan, as well as several
    // fields in sudogs blocked on this channel.
    //
    // Do not change another G's status while holding this lock
    // (in particular, do not ready a G), as this can deadlock
    // with stack shrinking.
    lock mutex
}

chanstruct定义上来看,有lock字段,再来看看chan的读写实现(简化代码):

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    // ...
    lock(&c.lock)
    // ...
    unlock(&c.lock)
    // ...
}

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    // ...
    lock(&c.lock)
    // ...
    unlock(&c.lock)
    // ...
}

chan的实现源代码看到,其读写内部均加了锁,实际上在关闭chan时内部也是加锁了,所以实际应用中,多个coroutine同时读写chan时不需要加锁。

作者:shaoyuan1943