22

Go中必须谈论的四个迷点

 4 years ago
source link: https://studygolang.com/articles/26480
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

很多熟悉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)

指向接口的指针是无意义的。

接口本身是类型,接口类型在runtime中大概是这样:

type eface struct {
    _type *_type    // 8bytes
    data  unsafe.Pointer    // 8bytes
}

其中_type是实现者(即实现了接口方法的struct),data是指向实现者的指针。那么,指向接口的指针是什么?

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生成指向接口的指针并调用接口方法。

关于接口的延申阅读: 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 时不需要加锁。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK