Go语言基础学习(二)

Go语言中代替枚举的办法

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

1. 常量中的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
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)
}

执行结果

1
2
3
4
5
6
7
Sun : 0
Mon : 1
Tues: 2
Wed : 3
Thur: 4
Fri : 5
Sat : 6

2. 将枚举值转换为字符串

使用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
49
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语言中的运算符

1. 语言的赋值运算符

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

import (
"fmt"
)

func main() {
var a = 28
var b = 99
a = b
fmt.Println(" a = b :", a)
a = 28
a += b
fmt.Println(" a += b :", a)
a = 28
a -= b
fmt.Println(" a -= b :", a)
a = 28
a *= b
fmt.Println(" a *= b :", a)
a = 28
a /= 2
fmt.Println(" a /= 2 :", a)
a = 28
b %= 1
fmt.Println(" b %= 1 :", a)
a = 28
a &= b
fmt.Println(" a &= b :", a)
a = 28
a |= b
fmt.Println(" a |= b :", a)
a = 28
a ^= b
fmt.Println(" a ^= b :", a)
a = 28
a >>= 2
fmt.Println("a >>= 2 :", a)
a = 28
b <<= 1
fmt.Println("b <<= 1 :", a)
}

2. 优先级

下表为各运算符的优先级示图,优先级从高到低

优先级 类别 运算符
1 括号运算符 ()
2 单目运算符 !、++、–
3 算数运算符 *、/、%
4 +、-
5 位移运算符 >>、<<
6 关系运算符 <、<=、>、>=
7 ==、!=
8 按位与 &
9 按位异或 ^
10 按位或 |
11 逻辑与 &&
12 逻辑或 ||
13 赋值运算符 =、+=、-=、*=、/=、 %=、 >=、 <<=、&=、^=、|=

Go语言的分支语句

1. If…else

在 Go 语言中 if 关键字的作用和其它大部分语言相同,是用于测试某个布尔表达式或布尔值是否为真的。若为真则执行 if 之后 {} 中的语句,否则跳过这些语句。如果存在第二个分支,则需要使用 else 关键字,还需要增加分支的话就需要使用 else if 关键字了。

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

import "fmt"

func main() {
a := 10
if a > 10 {
fmt.Println("a大于10")
} else if a < 0 {
fmt.Println("a小于0")
} else {
fmt.Println("a的值是", a)
}
}

在 Go 语言中,if 后的条件表达式,还有一种特别的写法,可以在条件表达式之前写一个赋值表达式。

if…else 的特殊用法:

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

import "fmt"

func main() {
if a := 10; a > 10 { //赋值表达式和条件表达式之间用;隔开
fmt.Println("a大于10")
} else {
fmt.Println("a的值是", a)
}
}

2. switch…case

Go 语言对 switch…case 的功能进行了扩展,它变得更加的通用。switch 之后可以什么都不带。case也无需是一个固定值,也可以是一个布尔表达式,而且每一个 case 都是一个 独立的代码块,执行完了之后立刻跳出 switch,不需要使用 break。所以可以把 if…else 完美的改写成 switch…case 的形式。

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

import "fmt"

func main() {
a := "A"
switch a {
case "A", "a":
fmt.Println("分数区间为90~100")
case "B", "b":
fmt.Println("分数区间为70~89")
case "C", "c":
fmt.Println("分数区间为0~70")
default:
fmt.Println("错误的评分")
}
}

switch…case Go 语言中的新用法:

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() {
a := 50
switch {
case a < 60:
fmt.Println("不及格")
case a < 80:
fmt.Println("良好")
case a <= 100:
fmt.Println("优秀")
default:
fmt.Println("分数最多为100分")
}

}

Go语言的循环语句

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

1. for循环语句

1.1 普通用法

在 Go 语言中,for 循环之后一样可以跟三个语句

单次表达式;条件表达式;末尾循环体

但是它不需要使用()来包裹这三个表达式,写法上更加的简洁。同时它也和其它语言一样,可以任意省略这三个表达式。

1
2
3
4
5
6
7
8
9
10
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)
}
}

1.2 代替 while 的用法

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

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

import "fmt"

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

2. for 语言中的break 和 continue

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

breakcontinue 的逻辑和语法类似

2.1 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 //跳出死循环
}
}
}

2.2 break跳出多层死循环

在其它语言中,你很难去控制跳出循环的层数,在 Go 语言中提供了一种break LOOP的写法,可以使循环跳出到 LOOP 所在的位置。

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. 通道的声明和定义

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

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

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

由于 chan 的底层是一个指针,在 Go 语言中指针初始值为空,是需要实例化的。make 就是实例化了 chan。

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

在这种情况下最好使用:= 来同时声明和创建:

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

2. 通道的使用

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

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

import "fmt"

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

3. 通道结合 select 流程控制

在 Go 语言中为了更方便的利用通道的功能,提供了一个仅用于通道的流程控制语句:select...case。使用这个语句可以同时监听数个通道,非常适合用于并发时的进程调度,或者模块之间的解耦合。下面通过一个简单的例子来了解一下 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 塞入通道中")
}
}
}

select 语句和 switch 语句一样,case 都只会执行一个,然后立刻跳出语句。

Go语言函数的返回值

Go 语言的函数的返回值也有些特殊,它可以一次返回多个返回值,Go 语言中如果是单返回值,可以直接在函数声明的括号后面增加数据类型,代码示例如下:

1
func Sum(a, b int) int //声明并定义一个返回值为int数据类型的函数

如果要返回多个返回值,则需要用括号包裹返回值,并使用,隔开所有返回值,代码示例如下:

1
func Sum(a, b int) (int,string) //声明并定义一个返回值为int和string数据类型的函数

返回的方式有两种。一种是用 return 关键字加返回值直接返回,一种是声明返回值变量,使用返回值变量直接返回。

1. 使用 return+ 返回值返回

Go 语言可以一次返回多个值,多个值直接跟在 return 关键字之后,使用,隔开即可。

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

import "fmt"

func SumAndProduct(a, b int) (int, int) {
c := a + b
d := a * b
return c, d // 同时返回c和d两个变量
}

func main() {
sum, product := SumAndProduct(1, 2) //接受多返回的函数时,也需要用逗号隔开变量
fmt.Println("a + b =", sum)
fmt.Println("a * b =", product)
}

2. 使用返回值变量返回

Go 语言还能直接在返回值处声明变量,这个变量的作用域是整个函数,并且会在 return 的时候直接返回。

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

import "fmt"

func SumAndProduct(a, b int) (c int, d int) {
c = a + b //因为c和d函数返回值处已经定义了,可以直接使用
d = a * b
return // 直接返回,c和d自动传出去
}

func main() {
sum, product := SumAndProduct(1, 2) //接受多返回的函数时,也需要用逗号隔开变量
fmt.Println("a + b =", sum)
fmt.Println("a * b =", product)
}

Go语言函数的 defer

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

1. defer 的逆序执行

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

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

import "fmt"

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

结果输出

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

2. defer 中的变量

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

1
2
3
4
5
6
7
8
9
10
11
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

3. 总结

  • defer先声明后执行的语句模式;

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

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

Go语言中的闭包

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

1. Go语言的匿名函数

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

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

import "fmt"

// 定义一个函数类型,值为一个匿名函数的变量;
var f = func() {
fmt.Println("匿名函数作为变量来使用")
}

func main() {
f() // 使用这个匿名函数

// 定义一个匿名函数。在这个函数后加上(),就可以直接使用这个匿名函数。
func() {
fmt.Println("匿名函数直接使用")
}()
}

输出结果

1
2
匿名函数作为变量来使用
匿名函数直接使用

2. 匿名函数引用外部变量

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

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

import "fmt"

func main() {
str := "Hello Golang!"
func() {
str = "Hello Go!" // 匿名函数直接操作了main函数之中的变量str
}()
fmt.Println(str)
}

输出结果

1
Hello Go!

上述例子简单的构造了一个闭包,在匿名函数中并没有声明或者定义str这个变量,但是可以直接操作,引用可main函数中的自由变量。

使用defer和闭包相结合,深入了解一下闭包中的引用外部变量。

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

import "fmt"

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

输出结果

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

学习defer的时候有了解到 defer 后变量是保留它在 defer 时的值,而不会被 defer 之后的代码改变,但是在这闭包的情况下,看起来defer的值已经被改变了,其实原因是闭包是 引用了这个变量,即 defer 时被保留下来的是这个 变量的地址 ,后续代码改变的不是地址,而是这个地址存储的值,所以后续代码对这个变量的操作,都会反应到defer中。

3. 总结

  • 闭包就是匿名函数引用外部变量
  • 闭包中引用的变量会被外部环境改变,同时闭包内对变量的改变也会影响到外部环境的使用

参考来源

http://www.imooc.com/wiki/golesson/


Go语言基础学习(二)
https://suiyideali.github.io/2023/04/23/Go语言基础学习(二)/
作者
m0ch4z
发布于
2023年4月23日
更新于
2023年4月28日
许可协议