Go语言学习的注意点

000_关于环境

Golang的官网是https://golang.org,不过因为众所周知的原因,国内不能正常访问,那么替代方案有这么几种:

  • 方案一:Google CN提供的镜像网站:该网站最大的好处就是原版英文,可以获得等同于原官网的体验
  • 方案二:中文版官网:该网站已经部分汉化了,可以帮助阅读英文有困难的同学们
  • 方案三:使用Golang起一个本地服务:安装Golang后,在命令行里输入godoc -http=:8080,并在浏览器访问http://localhost:8080/即可访问本地官网(英文),8080是端口号,可以更改,这种方式优点在于不依赖网络
  • 方案四:https://tip.golang.org/是golang.org的完全限定域名,由Google提供,目前中国大陆可以正常访问

跨平台编译

Golang有两个重要的环境变量GOOSGOARCH,其中GOOS指的是目标操作系统,它的可用值为:

  1. darwin
  2. freebsd
  3. linux
  4. windows
  5. android
  6. dragonfly
  7. netbsd
  8. openbsd
  9. plan9
  10. solaris

一共支持10中操作系统。GOARCH指的是目标处理器的架构,目前支持的有:

  1. arm
  2. arm64
  3. 386
  4. amd64
  5. ppc64
  6. ppc64le
  7. mips64
  8. mips64le
  9. s390x

一共支持9中处理器的架构

如果我们要生成不同平台架构的可执行程序,只要改变这两个环境变量就可以了(临时禁用CGO,以防出问题),比如要生成linux 64位的程序,命令如下:

1
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build github.com/xxx/hello

前面两个赋值,是更改环境变量,这样的好处是只针对本次运行有效,不会更改我们默认的配置。

以上这些用法差不多够我们用的了,更多关于go build的用法,可以通过以下命令查看:

1
go help build

此段摘自 飞雪无情博客

001_数据类型

rune类型

Golang没有Java里面的char类型,取而代之的是rune类型,Golang源码对该类型的注释是这样的:

rune is an alias for int32 and is equivalent to int32 in all ways.
译:rune是int32的别名,在所有方面都等同于int32。

因此,它在执行以下代码的时候,输出的结果并不是预期的字符a,而是数字97

1
2
3
4
func main() {
myChar := 'a'
fmt.Println(myChar) // 97
}

这个地方的对应关系是基于ASCII码表的,myChar是一个rune类型变量,而rune其实就是代表整数的int32类型,参照码表,a对应为十进制下的97,所以这里的输出结果是数字97

要输出字符a的话,需要进行类型转换,将rune类型转换为字符串类型的a

1
2
3
4
func main() {
myChar := 'a'
fmt.Println(string(myChar))
}

正因为这个特点,我们大概会遇到这个困扰:

如何将一个数字,例如:97转换为字符串使用?

如果我们套用PythonJava等语言的经验的话,大概会写出这样的代码:

1
2
3
4
5
func main() {
num := 97
s := string(num)
fmt.Println(s) // a
}

这样显然是不符合我们的预期的,而正确的方法是这样的:

1
2
3
4
5
func main() {
num := 97
s := strconv.Itoa(num)
fmt.Println(s) // 97
}

string类型

忽视转义字符,以纯文本输出

使用键盘1左边的键`而不是双引号,就可以实现对转义字符以纯文本输出

1
2
var name = `Ja\nck`
fmt.Println(name)

输出

1
Ja\nck

字符串的遍历

string是字符串类型,采用unicode编码,因此在表示非ASCII字符的时候,其大小不是一个字节,具体看示例:

1
2
3
4
func main() {
str := "我爱中国"
fmt.Println(len(str)) // 12
}

因此,我们在遍历的时候,不能直接使用其他编程语言里面常用的按照长度遍历,否则会是这样的结果:

1
2
3
4
5
6
func main() {
str := "我爱中国"
for i := 0; i < len(str); i++ {
fmt.Println(str[i])
}
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
230
136
145
231
136
177
228
184
173
229
155
189

使用string()转换类型:

1
2
3
4
5
6
func main() {
str := "我爱中国"
for i := 0; i < len(str); i++ {
fmt.Println(string(str[i]))
}
}

输出就是彻底的乱码:

1
2
3
4
5
6
7
8
9
10
11
12
æ
ˆ
‘
ç
ˆ
±
ä
¸
­
å
›
½

出现这个问题的原因就是,汉字是由多个字节来表示的,而Golang当中的len()是获取的字节长度,而非字符数

正确遍历一个字符串中每一个字符的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
str1 := "我爱中国"
for key, value := range str1 {
fmt.Println(key, string(value))
}
fmt.Println()

// 或者
str2 := "我爱中国"
for key, value := range []rune(str2) {
fmt.Println(key, string(value))
}
}

输出:

1
2
3
4
5
6
7
8
9
0 我
3 爱
6 中
9 国

0 我
1 爱
2 中
3 国

注意:前者的下标依然是按照字节来计算的,而后者则是按照字符

计算真实长度的方法:

1
2
3
4
5
func main() {
str := "我爱中国"
fmt.Println(utf8.RuneCount([]byte(str))) // 4
fmt.Println(utf8.RuneCountInString(str)) // 4
}

在不使用range的情况下遍历:

1
2
3
4
5
6
7
8
9
func main() {
str := "我爱中国"
strBytes := []byte(str)
for len(strBytes) > 0 {
ch, size := utf8.DecodeRune(strBytes)
strBytes = strBytes[size:]
fmt.Println(string(ch))
}
}

输出:

1
2
3
4




其他字符串操作常用函数

strings.Fields:按空格分隔

1
2
3
4
5
6
func main() {
str := " 我 爱 中 国"
fields := strings.Fields(str)
fmt.Println(len(fields)) // 4
fmt.Println(fields) // [我 爱 中 国]
}

int类型

关于内存地址

Golang当中的int类型的变量,在值改变的时候,其内存地址不会改变

具体示例如下:

例1:

1
2
3
4
5
func main() {
for i := 1; i <= 5; i++ {
fmt.Printf("i的值%d,i的地址%p\n", i, &i)
}
}

输出为:

1
2
3
4
5
i的值1,i的地址0xc000094000
i的值2,i的地址0xc000094000
i的值3,i的地址0xc000094000
i的值4,i的地址0xc000094000
i的值5,i的地址0xc000094000

例2:

1
2
3
4
5
6
func main() {
a := 1
fmt.Println(&a)
a = 2
fmt.Println(&a)
}

输出为:

1
2
0xc000094028
0xc000094028

数组和切片

数组和切片的不同

  • 切片是类似指针的引用类型,所以可以直接打印地址,而不需要取地址符号&
  • 数组定义时就会分配内存空间;切片不会
1
2
3
4
5
6
7
func main() {
var s1 [3]int
fmt.Printf("%p\n", &s1) // 0xc0000180c0

var s2 []int
fmt.Printf("%p\n", s2) // 0x0,没有分配内存
}
  • 数组可以和相同类型的数组直接比较,切片只能和nil作比较,刚刚定义的切片为nil
1
2
3
4
5
6
func main() {
s1 := []int{1, 2}
s2 := []int{1, 2}

fmt.Println(s1 == s2) // 这是错误的!
}
1
invalid operation: s1 == s2 (slice can only be compared to nil)

切片的扩容

当容量小于1024,当长度超出容量,则容量翻倍,如果翻倍后容量已经大于长度,则容量就是这个数字;如果容量依然小于长度,则容量等于长度

切片的删除元素

Golang并没有提供删除的相关功能,但是我们可以利用切片append实现

这是一个遍历集合,并删除特定元素的例子:

1
2
3
4
5
6
7
8
9
10
func main() {
s := []string{"a", "b", "c", "d"}

for i, value := range s {
if value == "c" {
s = append(s[:i:i], s[i+1:]...)
}
}
fmt.Println(s) // [a b d]
}

方式1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
// 声明一个切片,作为初始切片
s := []int{1, 2, 3, 4, 5}
fmt.Println("初始切片:", s)

// 现在我要删除索引为2的元素
n := 2

// 先取索引2前面的元素
newSlice := s[0:n]
fmt.Printf("索引n之前的部分:%v,内存地址为:%p\n", newSlice, newSlice)
fmt.Printf("此时原切片为:%v,内存地址为:%p\n", s, s)

// 再取索引2后面的元素,并和索引2前面的元素合并
newSlice = append(newSlice, s[n+1:]...)
fmt.Printf("索引n之前和索引n之后元素合并后的切片:%v,内存地址为:%p\n", newSlice, newSlice)
fmt.Printf("此时原切片为:%v,内存地址为:%p\n", s, s)
}

输出:

1
2
3
4
5
初始切片: [1 2 3 4 5]
索引n之前的部分:[1 2],内存地址为:0xc00001a0c0
此时原切片为:[1 2 3 4 5],内存地址为:0xc00001a0c0
索引n之前和索引n之后元素合并后的切片:[1 2 4 5],内存地址为:0xc00001a0c0
此时原切片为:[1 2 4 5 5],内存地址为:0xc00001a0c0

分析:

由于newSlice是从初始切片截取所得,其拥有相同的初始索引位置(索引位置:0)的值(值:1),索引newSlice的内存地址和原始切片的内存地址是相同的

切片的内存地址等于其第一个元素的内存地址,因为此处我们是从0号索引截取,所以snewSlice的内存地址相同

因此,当我们对newSliceappend操作时,原切片的值也被一起改变了,这并不好

方式2:

解决上述问题的思路就是,我们确实需要一个切片,它的初始元素等于原切片的初始元素,而内存地址不同,我们即想到了创建一个新的slice,而不是在原有slice(或其切片上)上直接操作

我们可以运用copy函数,来拷贝需要的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
// 声明一个切片,作为初始切片
s := []int{1, 2, 3, 4, 5}
fmt.Println("初始切片:", s)

// 现在我要删除索引为2的元素
n := 2

// 先取索引2前面的元素
newSlice := make([]int, n)
copy(newSlice, s[0:n]) // 此处的newSlice的内存地址不同于s了
fmt.Printf("索引n之前的部分:%v,内存地址为:%p\n", newSlice, newSlice)
fmt.Printf("此时原切片为:%v,内存地址为:%p\n", s, s)

// 再取索引2后面的元素,并和索引2前面的元素合并
newSlice = append(newSlice, s[n+1:]...)
fmt.Printf("索引n之前和索引n之后元素合并后的切片:%v,内存地址为:%p\n", newSlice, newSlice)
fmt.Printf("此时原切片为:%v,内存地址为:%p\n", s, s)
}

输出

1
2
3
4
5
初始切片: [1 2 3 4 5]
索引n之前的部分:[1 2],内存地址为:0xc00009e040
此时原切片为:[1 2 3 4 5],内存地址为:0xc000094000
索引n之前和索引n之后元素合并后的切片:[1 2 4 5],内存地址为:0xc0000a0020
此时原切片为:[1 2 3 4 5],内存地址为:0xc000094000

关于Set和去重

Golang是不提供set这种数据类型的,但是这也并没有什么,因为set完全就可以通过使用map来实现

我们在其他语言中,set最常用的就是去重,在Golang中,我们这样写即可:

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
// 这是一个带有重复元素的切片
s := []int{1, 2, 2, 3, 4, 5, 5, 6}
m := map[int]interface{}{}
for _, value := range s {
m[value] = struct{}{}
}

for key := range m {
fmt.Printf("%d\t", key) // 5 6 1 2 3 4
}
}

注意:无论是map还是set,都无法保证集合内元素的顺序和插入时的顺序一致,甚至Golang还刻意提高了其顺序的随机性

002_关于对象声明

Golang中常规变量声明有两种方式:

方式一:使用var关键字声明:

1
var name string

方式二:使用:=的方式声明

1
name := ""

不过值得注意的是,这两种方式还是存在区别的:

  • var关键字声明可以被用在全局位置。也就是说,:=这种方式只能够在函数内部使用
  • 我们知道Golang中的变量一经声明必须使用,否则会抛出xxx declared and not used的错误,不过全局变量可以声明而不使用

003_关于对象的初始化

此处参考飞雪无情的文章:Go语言中new和make的区别

尝试思考下面代码的执行结果,并分析原因:

1
2
3
4
5
func main() {
var i *int
*i = 10
fmt.Println(*i)
}

输出:

1
panic: runtime error: invalid memory address or nil pointer dereference

原因是这样的:当我们使用引用类型的时候,必须先分配其内存空间,然后我们存储的值才能有地方放

所以需要像这样:

1
2
3
4
5
func main() {
var i *int = new(int)
*i = 10
fmt.Println(*i) // 10
}

Golang源码中的new

1
2
3
4
5
6
7
// The new built-in function allocates memory. The first argument is a type,
// not a value, and the value returned is a pointer to a newly
// allocated zero value of that type.
// 分配内存的内置函数。接收的第一个参数是一个类型,而不是一个值
// 返回的值是一个指向new的指针
// 会分配这个类型的零值
func new(Type) *Type

make也是用于内存分配的,但是和new不同,它只用于chanmap以及切片的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。

make和new的异同

  • 二者都是内存的分配(堆上),但是make只用于slice、map以及channel的初始化(非零值);而new用于类型的内存分配,并且内存置为零
  • make返回的还是这三个引用类型本身;而new返回的是指向类型的指针

其实new不常用

所以有new这个内置函数,可以给我们分配一块内存让我们使用,但是现实的编码中,它是不常用的。我们通常都是采用短语句声明以及结构体的字面量达到我们的目的,比如:

1
2
i:=0
u:=user{}

这样更简洁方便,而且不会涉及到指针这种比麻烦的操作。

make函数是无可替代的,我们在使用slicemap以及channel的时候,还是要使用make进行初始化,然后才才可以对他们进行操作。

004_for…range

当需要在循环中改变值

当我们试图在for...range中改变一个切片的值的时候,会遇到这个问题:

1
2
3
4
5
6
7
8
9
func main() {
s1 := []int{1, 2, 3, 4, 5}
for _, value := range s1 {
if value == 3 {
value = 9
}
}
fmt.Println(s1)
}

输出:

1
[1 2 3 4 5]

其原因是value只是遍历出来的值的拷贝,要对其进行修改,需要使用下标:

1
2
3
4
5
6
7
8
9
func main() {
s1 := []int{1, 2, 3, 4, 5}
for key, value := range s1 {
if value == 3 {
s1[key] = 9
}
}
fmt.Println(s1)
}

输出:

1
[1 2 9 4 5]

当需要在循环中存储值

当我们试图存储被for...range遍历的内容的引用时,可能会出现这样的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
s1 := []int{1, 2, 3, 4, 5}
myMap := map[int]*int{}

for key, value := range s1 {
myMap[key] = &value
}

// 迭代输出myMap的内容
for key, value := range myMap {
fmt.Printf("key = %d, value = %d\n", key, *value)
}
}

输出:

1
2
3
4
5
key = 0, value = 5
key = 1, value = 5
key = 2, value = 5
key = 3, value = 5
key = 4, value = 5

原因其实很简单:value虽然在遍历过程中,其值一直在修改,但是它的内存地址其实一直都没有变,所以在最后一次循环前放入myMap的引用所存储的值也都被修改为最后的5

解决方法:其实也很简单,每次循环时创建一个新的变量来存储value的值就可以了,这样放入myMap的内存地址每次也都是不同的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
s1 := []int{1, 2, 3, 4, 5}
myMap := map[int]*int{}

for key, value := range s1 {
temp := value
myMap[key] = &temp
}

// 迭代输出myMap的内容
for key, value := range myMap {
fmt.Printf("key = %d, value = %d\n", key, *value)
}
}

输出:

1
2
3
4
5
key = 2, value = 3
key = 3, value = 4
key = 4, value = 5
key = 0, value = 1
key = 1, value = 2

当在循环中遇到闭包

如果我们执行以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
s1 := []int{1, 2, 3, 4, 5}
fs := [5]func(){}
for key, value := range s1 {
fs[key] = func() {
fmt.Println(value)
}
}
// 执行
for _, value := range fs {
value()
}
}

则会输出:

1
2
3
4
5
5
5
5
5
5

为什么不是输出1 2 3 4 5呢?因为,在匿名函数中,fmt.Println(value)语句的value不是该匿名函数内声明的,而是从外层函数那里获得的引用

没错,闭包内的变量和闭包外的是相同的内存地址,而不是值拷贝

005_关于常量

  • 数值类型常量如果不明确指定类型可以直接运算(如:float64和int32)
  • 常量可以声明但不使用
  • 常量的声明只需要使用=,而不是:=或者var

关于iota

注意:iota无论你是否使用,其值都是从0依次递增,直到遇到新的constiota的值即重制为0

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
const (
a = 9 // iota值为0,虽然没有被使用,iota依然存在
b = iota // iota值为1
c // iota值为2,没有明确声明,继续使用b的表达式
d = 10 // iota值为3,虽然没有被使用,iota依然存在
e = iota // iota值为4
f // iota值为5,没有明确声明,继续使用e的表达式
)
const g = iota // 新的const,iota重制为0

fmt.Printf("a = %d\nb = %d\nc = %d\nd = %d\ne = %d\nf = %d\n", a, b, c, d, e, f)
fmt.Println("g =", g)
}

输出

1
2
3
4
5
6
7
a = 9
b = 1
c = 2
d = 10
e = 4
f = 5
g = 0