分享:你可能不知道的Go

Aug 27 2019 Golang

本文是根据我在公司分享的《你可能不知道的 Go(上)》而整理的文章

本次分享的主题是《你不知道的 Go》,其实这个名称来源自《你不知道的 JavaScript》这本书,当然起这个名字有些“妄自尊大”,再者这次分享确实没有什么“高大上”的内容,只是一些大部分初学者不会注意到的盲点,所以之后我就改名为《你可能不知道的 Go》。(笑)

Type

首先,第一个话题是 Go 中的类型。如下面的代码所示,我们声明了一个新类型 Number,当然平时我们都是声明一个新的 struct 或者 interface。

1
2
3
4
5
6
7
8
9
10
11
type Number float64

var i float64
var j Number

// Error: cannot use i (type float64) as type Number in assignmentgo
j = i

// OK
j = Number(i)
j = 1.02

对于为什么 i 和 j 不能互相赋值,大部分 Gopher 都能说出,因为二者是不同的类型,而 Go 是强类型语言,不能隐式转换,所以二者不能进行赋值。

通过这个例子可以说明,在 Go 中使用 type 就可以声明一个新类型,而不同的类型名称就代表不同的类型,即使他们底层类型是一致的。到这里,基本上所有的文档或者教程都对此有基本的说明。

再说一个例子,如下所示:

1
2
3
4
5
6
7
type Hash []byte
var i []byte
var j Hash

// OK!
i = j
j = i

Hash 类型 i 和 []byte 类型 j 应该是不同的类型,根据我们上一页的说明,二者应该是不能互相赋值的,为什么这里就没有编译错误?

回答这个问题很简单,因为我们自以为的结论是错的,这里的 i 和 j 是同一类型(identical)。

这样说,肯定会有人疑惑,为什么在这里就是同一类型的呢?因为 []byte 是一个未声明的类型,对于未声明的类型,他们是没有名字的,所以只要其底层类型一致就可以,比如上面的 slice,元素都是 byte ,所以二者是相同的类型,可以互相赋值。

为什么说 []byte 是未声明的类型,而 float64 则是已声明的类型,其实这个已经是定义好的了,我们可以在官方文档找到所有的已定义的类型。

image

这里补充一句,大家可以看到 int 也是定义的类型,而不是关键字,而在 Java 和 c++ 是关键字的,所以下面的代码是不会编译失败的,而且运行也是成功的:

1
2
3
4
5
6
7
8
9
package main

import "fmt"

var int = "相信我,这里不会编译错误"

func main() {
fmt.Println(int)
}

接着说未声明的类型。其它的比如 map array 等与其长度等定义相关,不能直接定义(长度的选择是无限的),所以这些都是未定义类型。我们可以在官方 spec 找到如何判断未定义类型二者是否类型一致:

不过上面还没有解释为什么 1.02 可以直接赋值给 Number。其实是因为 1.02 是一个无类型的浮点数字面量,注意这里的无类型是说没有类型名称。所以 1.02 可以代表底层类型为 float64 的 Number 类型,如果使用字符串字面量给其赋值就会编译失败。

关于这里可赋值性的概念,在官方文档中也有说明,基本就是上面所说明的内容,具体的大家可以看文档。

讲完这一段,大家就可以回答这个问题了,下面的的代码中赋值语句,哪个是对的,哪个是错的。

1
2
3
4
5
6
7
8
9
10
type Type0 []string
type Type1 []string

var x []string
var y Type0
var z Type1

y = x
z = x
y = z

第一个, y = x 由于 x 是未声明类型,而且与 y 底层类型一致,所以是可赋值的;第二个和第一个的原因一样;第三个,不能赋值,因为 y 和 z 是不同的类型。

当然这种方式会有很大的困扰,如果就想定义一个代表 float64 的 Number 类型怎么办?

也很简单,可以使用类型别名,类型别名代表二者类型完全一致。

1
2
3
4
5
type Number = float64
var i float64
var j Number
// OK
i = j

其类型方法也可以直接调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

type Mutex struct {
}

func (m *Mutex) Lock() {}
func (m *Mutex) Unlock() {}

type PtrMutex = *Mutex
type NewMutex = Mutex

func main() {
var a PtrMutex
a.Lock()

var b NewMutex
b.Lock()
}

在 go 也有内置的类型别名,比如说代表一个字节的 uint8 的别名就是 byte,而代表一个 unicode code pint 的就是 int32。

Slice

今天的第二个主题是 Slice,也就是切片。首先大家看下这段代码,函数参数接收一个 slice,然后在函数内部进行 append,最后输出什么?

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func add(i []int) {
i = append(i, 3)
}
func main() {
i := []int{1, 2}
add(i)
fmt.Println(i)
}

这个其实在 Twiiter 上有过类似的投票,选择输出 [1 2 3] 居多,大多数人留言说 slice 是引用类型,应该会修改原有的数据。

最后的打印结果是 [1 2],也就是说 slice 不是引用类型。准确地说,Go 里面参数传递只有拷贝传递,没有引用传递,更不会有引用类型之说。

其实 slice 是一个称之为胖指针的数据结构。什么是胖指针?看下面这副图,这个是 slice 的内部结构,可以看到除了一个指针之外,还有长度和容量两个字段,记录指针对象额外信息的结构就可以称之外胖指针。

go-slice

从这张图,然后我们再次分析下上一页的代码,由于 go 只有拷贝传递,所以 slice 内部的结构都会被赋值一份。当 append 的时候会插入一个元素,首先检查这里容量是否足够足够,这里因为直接定义的 slice 有两个元素,所以容量也是 2,所以会新开辟一块新的,然后函数内部 i 变量指向这块内存,而外部变量 i 还是指向原先的内存。

好了,如果我们假设有足够的容量呢,如下所示,append 会不会修改?也不会。

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func add(slice []int) {
slice = append(slice, 1, 2)
}
func main() {
slice := make([]int, 1, 10)
add(slice)
fmt.Println(slice)
}

为什么?append 增加元素的同时,一定会修改内部长度字段,而又因为 go 只有拷贝复制,长度变化不会影响到外部的长度,外部仍旧是长度为 1 的切片。

那如何修改 slice 呢?有两种方式,第一种传递指针,那么整体修改肯定会修改外部;另一种是大家常见的修改索引位置的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

func main() {
data := []string{"1", "2"}

var changeItem = func(data []string) {
data[0] = "10"
}
changeItem(data)

var changeItemByPoint = func(data *[]string) {
*data = append(*data, "3")
}
changeItemByPoint(&data)
}

除了现在这种使用 make 或者从数组内截取的或者使用字面量定义 slice,go 语言中还有一个结构也是 slice,那就是函数的可变参数。

如下所示,根据上述说明也会修改原有的结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
data := []string{"good", "evening"}
test(data...)
fmt.Println(data)
}

func test(args ...string) {
args[0] = "hello"
args[1] = "world"
}

那么如果我们简单的一个一个传递会怎么样?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
data := []string{"good", "evening"}
test(data[0], data[1])
fmt.Println(data)
}

func test(args ...string) {
args[0] = "hello"
args[1] = "world"
}

如果以这样方式传递的话,go 会先构建一个 slice,然后按照顺序存储参数,所以这种不会影响原有数据。

string 类似 slice,不过内部并没有 cap 容量这个字段,主要是因为字符串内部是不可变的。

image

通过上图的字符串内存结构可以看到,底层也是一个字节数组。

如果使用 for…range 的方式遍历字符串,每个元素是不是一个 byte 呢?这个不是的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"reflect"
)

func main() {
raw := "我喜欢Go语言"
for i, v := range raw {
fmt.Println(i, reflect.TypeOf(v), v)
}
// 0 int32 25105
// 3 int32 21916
// 6 int32 27426
// 9 int32 71
// 10 int32 111
// 11 int32 35821
// 14 int32 35328
}

根据输出,我们可以看到,每个元素都是 int32,根据我们上面所说明的,int32 有个别名是 rune,而 rune 是一个 unicode code point,可以表示字符。

如果想打印输入一个字符,而不是一个数值,那么可以使用 string 将 v 进行转换即可。

大家也注意到,虽然每个元素都是按照 rune 来输出的,但是索引却不是,如果我们要一个 rune 对应一个索引的话,可以使用下面的方式,将 string 转化为 []rune:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
)

func main() {
raw := "我喜欢Go语言"
for i, v := range []rune(raw) {
fmt.Println(i, string(v))
}
// 0 我
// 1 喜
// 2 欢
// 3 G
// 4 o
// 5 语
// 6 言
}

当然上面所说仅对 for…range 的方式有效,如果用 for 循环的方式,根据 string 长度,这个时候每个元素就会是 byte 类型。

Error

go 的 error 是一个接口,接口的零值是 nil。

1
2
3
type error interface {
Error() string
}

其实大家也知道除了接口外,函数、指针等的零值也是 nil。

这个引发了一个问题,新手经常会犯的错误,也就是所谓的 nil error != nil

我们看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

type MyError struct{}

func (e *MyError) Error() string {
return "any error"
}

func test() error {
var myErr *MyError
return myErr
}

func main() {
fmt.Println(test() == nil) // false
}

为什么呢?不是说指针的零值也是 nil 么?

我们先看下接口的内存接口,其实 go 中的接口结构类似 Java 中的对象,都包括元数据和数据的指针。

image

看到这里,我们再分析下之前的代码,test 函数中,myErr 是一个 MyError 类型的指针,但是这个数据并不是一个接口类型,需要进行转化成接口,那转换成接口怎么处理呢,就是上述我们说的填充元数据和数据的指针。

这样说大家就应该明白了,非接口的 nil 的数据在转换成接口的时候就不是 nil 了。

Enum

今天的最后一个话题是枚举,大家应该都觉得这个很简单,没什么可说的。

嗯,确实是这样。不过,大家可以试试这个,说下面的所有枚举值是多少。

image

我觉得大多数人第一次看到这个应该懵逼的,当然我也是这样。

首先我们看下是否能编译,这里有多个 iota ,还有一个 float64 的枚举,实话讲,刚看到这个对于能不能编译我也不能确认。这里是能编译成功的。

然后再看具体的值,首先 x 一定是可以确认的,是 0,那么 y 呢?是 0 还是 1 呢?有经验的小伙伴一定知道还是 0。

那么知道这个的话,下面就好说了,a b c 分别代表 0 1 2。

接下来,下面的 d 已经定义,那么就是 1,那 e 呢,是 1 还是 2?正确答案是 1,因为 d 占据了 iota 为 0 的位置,f 那么就是 2。

接下来,g 是 100,那么 h 呢?正确答案是和 g 一样 100。之后 i 呢? iota 又回到了正确的位置,代表其位置,变成了 5。

接下来,看最后一列,j 和 k 根据上面可以推断出为 0 和 1。那么 l 呢?正确的是 2,属于同一列的变量都是 iota 的位置的数据。

关于 iota 的说明,大家可以在 spec 中找到详细说明,这里就不再赘述了。

OK,这就是今天所有分享的内容,感谢大家参加!