go语言学习笔记(1)

GO 语言学习

开始了GO语言的学习,做点自认为有意思的笔记。
笔记摘抄自慕课教程 Go入门教程


Go 语言的 :=

单变量 :=

Go语言中新增了一个特殊的运算符 :=,可以是变量在未声明的情况下被赋值使用。
使用方法与带值声明变量类似,只是少了var关键字,使用例子如下:

1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
helloWorld := "Hello World!"
fmt.Println(helloWorld)
}

多变量 :=

在 Go 语言的多变量赋值体系中,也支持了:=运算符。你可以使用形如变量名,变量名:=变量值,
变量值的形式来进行多变量操作。其使用方法和多变量带值声明类似,只是少了var关键字。

1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
hello, World := "Hello", "World!"
fmt.Println(hello, World)
}



Go语言的整形(int)数据类型

定长类型

类型 范围
int8 -128~127
int16 -32768~32786
int32 -2147483648~2147483647
int64 -9223372036854775808~9223372036854775807



不定长类型

num int ``` 这个类型在32位的系统中长度和int32一致,在64位的系统中长度和int64一致。
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
<br></br>

## 类型转换
Go 语言是强类型语言,各个虽然同是整型,但是不同整型的变量不允许直接赋值,需要进行强制类型转换,同时长度大的整型向长度小的整型转换时,要考虑溢出问题。而且不同类的整型无法进行运算,想要进行运算必须要进行强制类型转换,使他们都变成同一类型之后,再运算。举一个int8和int16类型的变量赋值与计算的例子:
```Go
package main

import (
"fmt"
"math"
)

func main() {
var a int8 = math.MaxInt8
var b int16 = math.MaxInt8 + 1
fmt.Println(a, b)

a = int8(b)
fmt.Println(a, b) //因为b长度过长,在转换为int8的过程中溢出了

//整型变量可以和常数进行计算
a = a + 1
b = b + 1
fmt.Println(a, b)

//不同类型的整型变量计算必须强转为相同类型,一般转换为长度大的来计算
c := int16(a) + b
fmt.Println(c)
}



GO语言中代替枚举的办法

枚举类型用于声明一组命名的常量,当一个变量有几种可能的取值时,可以将它定义为枚举类型。在 Go 语言中,并没有提供枚举类型,但是枚举类型又是开发过程中非常重要的数据类型。因为它可以事先定义一组有效的值,可以有效地防止用户提交无效数据,抽象到业务场景中就是我们平时网页上遇到的下拉框,或者我们选择快递地址时的省市信息,均为枚举类型的用武之地。所以在 Go 语言中对常量进行了一定的扩展,使其可以完美地替代枚举类型。


常量中的iota

为了使常量可以更好地替代枚举类型,Go 语言提供了一个iota关键字。使用iota初始化常量,可以生成一组规则类似的常量,但是不用每个常量都写一遍表达式。在一个const()表达式中,从iota开始的常量会被置为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
27
28
package main

import (
"fmt"
)

type Weekday int //自定义一个星期类型,作为枚举类型

const (
Sun Weekday = iota
Mon
Tues
Wed
Thur
Fri
Sat
)

func main() {
fmt.Println("Sun :", Sun)
fmt.Println("Mon :", Mon)
fmt.Println("Tues:", Tues)
fmt.Println("Wed :", Wed)
fmt.Println("Thur:", Thur)
fmt.Println("Fri :", Fri)
fmt.Println("Sat :", Sat)
}



将枚举值转换为字符串

使用iota是可以使用 Go 语言的常量代替枚举类型,但是由于输出值均为阿拉伯数字,给调试和辨识造成了一定的困难。为了解决这一问题,Go 语言还提供了使常量枚举值输出为字符串的办法。需要我们手动构造一个使用枚举类型输出字符串的方法。

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
package main

import (
"fmt"
)

type Weekday int //自定义一个星期类型,作为枚举类型

const (
Sun Weekday = iota
Mon
Tues
Wed
Thur
Fri
Sat
)

func (w Weekday) String() string {
switch w {
case Sun:
return "Sun"
case Mon:
return "Mon"
case Tues:
return "Tues"
case Wed:
return "Wed"
case Thur:
return "Thur"
case Fri:
return "Fri"
case Sat:
return "Sat"
}
//不存在的枚举类型就返回"N/A"
return "N/A"
}

func main() {
fmt.Println("Sun :", Sun)
fmt.Println("Mon :", Mon)
fmt.Println("Tues:", Tues)
fmt.Println("Wed :", Wed)
fmt.Println("Thur:", Thur)
fmt.Println("Fri :", Fri)
fmt.Println("Sat :", Sat)
}

注:不常使用,稍作了解即可。


GO语言的循环语句

在GO语言中减少了循环语句的关键字,仅有一个for关键字
但不仅没有减少其功能,而且还兼容了其他语言中的while关键字的用法,甚至更强大。


for循环语句

普通用法

在GO语言中,for循环之后一样可以跟三个语句(单次表达式; 条件表达式; 末尾循环体)

但是它不需要()来包裹这三个表达式,写法上也更加简洁。例子如下:

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
for a := 0; a < 10; a++ {
fmt.Println(a)
}
}
1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
a := 0
for ; a < 10; a++ {//表达式可以省略,但是;不能省略
fmt.Println(a)
}
}

代替while的用法

在其它大部分语言中 for 循环中省略单次表达式和末尾循环体其实就和其它语句中的 while 用法一致了。
所以在 Go 语言中,直接使用省去单次表达式和末尾循环体的 for 循环语句来代替 while 的用法.
为了简便代码书写,Go 语言中 for 循环语句在省去单次表达式和末尾循环体时,可以不写分号。

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

import "fmt"

func main() {
a := 0
for a < 10 { //和其它语言中的while(a<10)一致
fmt.Println(a)
a++
}
}

for语言中的breakcontinue

在我们的生产环境中,经常需要用到死循环的场景。所以在 Go 语言中新增了一个 for 关键字死循环的用法,让程序员不用写无聊的for(;;){}do{} while(1)同时可以使用breakcontinue来控制循环。breakcontinue的逻辑和语法类似,故笔记只有break

break跳出单层死循环

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

import "fmt"

func main() {
a := 0
for { //死循环的写法就是省略 单次表达式;条件表达式;末尾循环体
fmt.Println(a)
a++
if a >= 10 {
break //跳出死循环
}
}
}



break跳出多层死循环

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

import "fmt"

func main() {
a := 0
LOOP:
for {
for {
fmt.Println(a)
a++
if a >= 10 {
break LOOP //跳出循环至LOOP所在的层级
}
}
}
}



Go语言的通道

Go 语言中有一个特殊的类型 chan,这是在 Go 语言的多线程操作中非常重要的一个数据类型。它的一般用于线程之间的数据传输,所以这个操作类型叫做 “ 通道 (channel)”。


通道的声明和定义

通道可以理解为一种特殊的变量,所以它的声明和其它变量的声明并没有太大的区别,声明通道变量的写法如下:

1
var c chan int //声明一个存放int类型数据的通道

声明之后不能被直接使用,要通过内置函数make()来创建一下通道变量才可以使用:

1
2
var c chan int //声明一个存放int类型数据的通道
c = make(chan int, 1) //创建一个长度为1的通道

所以一般最好使用:=来同时声明和创建:

1
c := make(chan int, 1) //声明并创建一个存放int类型数据的通道



通道的使用

在 Go 语言中,使用 <- 符号来向通道中塞取数据。放在通道右边 chan <-,就是塞数据,放在通道左边 <- chan ,就是取数据。

1
2
3
4
5
6
func main() {
c := make(chan int, 1)
c <- 10 //将10塞入通道中
i := <-c //将10从通道中取出,并赋值给变量i
fmt.Println(i)
}



通道结合 select 流程控制

在 Go 语言中为了更方便的利用通道的功能,提供了一个仅用于通道的流程控制语句:select...case 使用这个语句可以同时监听数个通道,非常适合用于并发时的进程调度,或者模块之间的解耦合。

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

import "fmt"

func main() {
c := make(chan int, 1)

for a := 0; a < 2; a++ {
select {
case i := <-c:
fmt.Println("从通道中取出", i)
case c <- 10:
fmt.Println("将 10 塞入通道中")
}
}
}



Go语言函数中的defer

在 Go 语言中的 defer 关键字就是 Go 语言中延迟语句的标志。Go 语言会在函数即将返回时按逆序执行 defer 后的语句。也就是说先被 defer 的语句后执行,最先执行最后被 defer 的语句。defer 和有些语言中的 finally 语句块的用法类似,一般都用于释放一些资源,最常用的地方就是进程锁的释放。


defer的逆序执行

defer会在函数即将结束的时候执行,而且是按照defer的顺序逆序执行。

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

import "fmt"

func main() {
defer fmt.Println("第一个defer的语句")
defer fmt.Println("第二个defer的语句")
defer fmt.Println("第三个defer的语句")
fmt.Println("Hello Codey!")
}

输出结果:

1
2
3
4
Hello Codey!
第三个defer的语句
第二个defer的语句
第一个defer的



defer中的变量

defer关键字之后若有变量,则defer记录的是在defer时的变量值,而不是最后函数结束时的变量值:

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

import "fmt"

func main() {
a := 10
defer fmt.Println("defer时a的值为", a)
a = 100
fmt.Println("print时a的值为", a)
}

输出结果:

1
2
print时a的值为100
defer时a的值为10



defer注意小结

defer是先声明后执行的语句模式。

defer会在函数即将结束的时候统一执行。

defer中的变量值是不会被defer之后的语句改变。



Go语言中的闭包

简单的说 Go 语言的闭包就是一个引用了外部自由变量的匿名函数,被引用的自由变量和该匿名函数共同存在,不会因为离开了外部环境就被释放或者删除,还可以在这个匿名函数中继续使用。


Go语言的匿名函数

匿名函数,顾名思义,就是隐藏函数名的函数。

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

import (
"fmt"
)

var f = func() {
fmt.Println("匿名函数作为变量来使用")
}

func main() {
f()

func() {
fmt.Println("匿名函数直接使用")
}()
}



匿名函数引用外部变量

如果在匿名函数内,使用了外部环境的变量,就构成了一个闭包。简单来讲就是一个函数内,使用匿名函数来操作函数内声明的变量。

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

import (
"fmt"
)

func main() {
str := "Hello World!"
func() {
str = "Hello Codey!"
}()
fmt.Println(str)
}

运行结果:

1
Hell Codey!

上述例子简单的构造了一个闭包,在匿名函数中并没有声明或者定义str这个变量,但是可以直接操作,就是引用可main函数中的自由变量。这个例子可能对自由变量的引用表现不是很直观,我们接下来使用defer和闭包相结合,深入了解一下闭包中的引用外部变量。


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

import (
"fmt"
)

func main() {
str := "Hello World!"
defer func() {
fmt.Println("defer str=", str)
}()
str = "Hello Codey!"
fmt.Println("main str=", str)
}

运行结果:

1
2
main str = Hello Codey!
defer str = Hello Codey!

前文提defer的时候明确介绍了defer 后变量是保留它在defer时的值,而不会被defer之后的代码所改变。但是在闭包这边这个看起来不太适用,其实是适用的,只是闭包是引用了这个变量,也就是说,在defer时被保留下来的是这个变量的地址,后续代码改变的不是地址,而是这个地址存储的值,所以后续代码对这个变量的操作,都会反应到这个 defer中。


Go语言中的切片

Go语言中可变长度的”数组”——切片slice


切片的创建

切片的声明方式和数组类似,写法上看就是声明一个没有长度的数组:var 切片名 []切片类型。其中切片类型可以是切片本身,也就是切片的切片,就构成了多维的切片。切片在使用之前必须要初始化,它没有零值。声明后它的值是 nil,这是因为它的底层实现是一个指向数组的指针,在你给它存入一个数组的地址之前,它只能是 nil。

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

import (
"fmt"
)

func main() {
var a []int
fmt.Println("初始化前:", a)
a = make([]int, 5, 10)
fmt.Println("初始化后:", a)
a[4] = 5
fmt.Println(" 赋值后:", a)
a[5] = 6
fmt.Println("赋值后:", a)
}



切片的截取

切片之所以被叫做切片是有原因的,它可以从任意长度开始切,切到任意长度为止,然后这一段拿出来就是一个新的切片。
切割形式为 切片名(s)[起始下标(begin):结束下标(end):最大容量(max)]

切片截取形式表
|操作| 含义|
| —- | —- |
|s[begin?max] | 截取切片s从begin到end的数据,构成一个容量为max-begin,长度为begin-end的切片。(用的不多)|
|s[begin:end] | 截取切片s从begin到end的数据,构成一个容量和长度均为begin-end的切片。|
|s[begin:] | 截取切片s从begin到最后的数据,构成一个容量和长度均为len(s)-end的切片。|
|s[:end] | 截取切片s从0到最后的数据,构成一个容量和长度均为end-0的切片。|

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

import (
"fmt"
)

func main() {
var a = []int{1, 2, 3, 4, 5}
fmt.Println("a[1:3]=", a[1:3])
fmt.Println("a[1:]=", a[1:])
fmt.Println("a[:3]=", a[:3])
}



切片的追加

切片使用一个 Go 语言的内置函数append(切片,待添加的值),来进行切片末尾元素的追加。

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

import (
"fmt"
)

func main() {
var a = []int{1, 2, 3, 4, 5}
a = append(a, 6)
fmt.Println(a)
a = append(a, 7, 8)
fmt.Println(a)
b := []int{9, 10}
a = append(a, b...)
fmt.Println(a)
}



切片的长度和容量

在切片中可以使用len()获取切片中元素的数量,也就是切片的长度。使用cap()可以获取切片引用的数组的长度,也就切片的容量。切片的容量一般大于等于长度,容量会随着长度的增长而增长。
在初始化一个切片的时候其实时给切片引用了一个数组,然后容量就是这个数组的长度,然后如果切片的长度超过了切片的容量,它就会让切片引用一个容量更大数组来存放这些元素。

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

import (
"fmt"
)

func main() {
var a = []int{1, 2, 3, 4, 5}
fmt.Printf("a的地址%p,a的长度%d,a的容量%d\n", a, len(a), cap(a))
a = append(a, 6)
fmt.Printf("a的地址%p,a的长度%d,a的容量%d\n", a, len(a), cap(a))
a = append(a, 7, 8)
fmt.Printf("a的地址%p,a的长度%d,a的容量%d\n", a, len(a), cap(a))
b := []int{9, 10, 11}
a = append(a, b...)
fmt.Printf("a的地址%p,a的长度%d,a的容量%d\n", a, len(a), cap(a))
}

能从执行结果看到,在切片a每次添加的元素要超过它的容量时,它的地址就会发生改变,其实就是让它引用了一个新的容量更大的数组。


小结

切片在使用前需要初始化。

切片的本质是一个指针数组,但是它的地址会随着长度超过容量而改变。

在应用场景中一般都使用切片。



Go语言中的Map

map是一种元素对的无序集合,每一个索引(key)对应一个值(value)。map 是一种能够通过索引(key)迅速找到值(value)的数据结构,所以也被称为字典。在 Go 语言中因为线程安全问题,一共实现了两种类型的map


无锁的map

这种类型的 map 是线程不安全的 map,多个线程同时访问这个类型的 map 的同一个变量时,会有读写冲突,会导致系统奔溃。所以一般在单线程程序中使用的较多。

map的创建

map 的底层结构也是一个指针,所以和变量不同,并不是声明后立刻能够使用。和切片相同,需要使用make()函数进行初始化。在初始化之前为空,没有零值。

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

import (
"fmt"
)

func main() {
var m map[string]string
fmt.Println(m == nil)
m = make(map[string]string)
fmt.Println(m == nil)
}



map的赋值

map 的赋值有两种方式:

使用:=使map在定义的时候直接赋值。

使用map[key]=value的形式对map进行赋值。

在明确知道 map 的值的时候就可以使用第一种方式进行赋值,比如说在建立中英文对应关系的时候。在未知 map 的取值时,一般建议使用后者进行赋值。

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

import "fmt"

func main() {
m1 := map[string]string{"Apple": "苹果", "Orange": "橘子", "Banana": "香蕉"}
fmt.Println(m1["Apple"])
m2 := make(map[string]string)
m2["Apple"] = "苹果"
m2["Orange"] = "橘子"
m2["Banana"] = "香蕉"
fmt.Println(m2["Apple"])
}



map的遍历

map 是字典结构,如果不清楚所有 key 的值,是无法对 map 进行遍历的,所以 Go 语言中使用了一个叫做range的关键字,配合for循环结构来对map结构进行遍历。

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

import "fmt"

func main() {
m := map[string]string{"Apple": "苹果", "Orange": "橘子", "Banana": "香蕉"}
for k, v := range m {
fmt.Println("key:", k, ", value:", v)
}
}

注意:map是无序的,所以每次输出的顺序可能会不一样。


map的删除

map 在普通的用法中是无法移除只可以增加 key 和 value 的,所以 Go 语言中使用了一个内置函数delete(map,key)来移除 map 中的 key 和 value。

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

import "fmt"

func main() {
m := map[string]string{"Apple": "苹果", "Orange": "橘子", "Banana": "香蕉"}
fmt.Println(m)
delete(m, "Apple")
fmt.Println(m)
}



文章作者: 牧尾伊織
文章链接: http://example.com/2021/09/05/note/go语言学习笔记(1)/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Makiori's blog