Go入门与实战

GO 语言

一、基本语法知识

1. Go语言的基本组成

先来看一段入门代码

1
2
3
4
5
package main
import "fmt"
func main() {
fmt.Println("Hello, PP")
}

1.1 包声明

1
package main

定义了包名。通常需要在源文件第一行指明文件属于哪个包。

package main 表示一个可独立执行的程序,每个工程都应包含一个名为main的包。

1.2 引入包

1
import "fmt"

告诉编译器程序需要使用fmt包中的东东(函数、变量或其他元素)。

fmt包实现了格式化IO的函数

1.3 函数

1
2
3
func main() {
fmt.Println("Hello, PP")
}

这是程序开始执行的函数。main()函数式每一个可执行程序必须包含的,通常在程序启动后立即执行。

1.4 变量

变量的声明和初始化

a.标准格式

1
var <name> <type>

var是关键字

使用 var ,虽然只指定了类型,但是 Go 会对其进行隐式初始化,比如 string 类型就初始化为空字符串,int 类型就初始化为0,float 就初始化为 0.0,bool类型就初始化为false,指针类型就初始化为 nil。

也可以在声明的同时初始化

1
var num int = 20

go会对右边的值进行类型判断,因此我们也可以在声明时省去类型,简写为

1
var num = 20

内部变量(用在函数内部)和全局变量均适用

b.同时声明多个变量

1
2
3
4
5
var(
num int
name string
grade float32
)

内部变量和全局变量均适用

c.短类型声明法

使用:=声明变量,可以显式初始化。变量和常量通常都只能声明一次。也有例外,匿名变量可以多次声明。

1
num := 20

只能用于函数内部

声明初始化多个变量

1
num, name := 20,"weipp"

d.声明指针变量

go语言提供了new函数来声明指针变量

1
new(Type)

这将创建一个Type类型的匿名变量,初始化为Type类型的初始值(go语言默认),返回值为匿名变量地址,指针类型为&Type

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
ptr := new(bool)
fmt.Println("ptr address: ", ptr)
fmt.Println("ptr value: ", *ptr) // * 后面接指针变量,表示从内存地址中取出值
}

输出

1
2
ptr address:  0xc000016088
ptr value: false

可以看出,用new创建指针变量和用&创建指针变量能达到一样的效果,除了不需要使用具体的变量名。上面的代码用普通方法创建指针变量可以写为:

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

import "fmt"

func main() {
var boolean bool
ptr := &boolean
fmt.Println("ptr address: ", ptr)
fmt.Println("ptr value: ", *ptr) // * 后面接指针变量,表示从内存地址中取出值
}

匿名变量

称作占位符,或者空白标识符,用下划线_表示。它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。

特点:

1.不分配内存,不占用内存空间

2.可以多次声明

3.解决命名的烦恼

通常我们用匿名接收必须接收,但是又不会用到的值。

1
2
3
4
5
6
7
8
func GetData() (int, int) {
return 100, 200
}
func main(){
a, _ := GetData()
_, b := GetData()
fmt.Println(a, b)
}

输出

1
100 200

2.变量的作用域

局部变量

在函数体内声明的,作用域只在函数体内。

局部变量不是一直存在的,它只在定义它的函数被调用后存在,函数调用结束后这个局部变量就会被销毁。

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

import (
"fmt"
)

func main() {
//声明局部变量 a 和 b 并赋值
a := 3
var b int = 4
//声明局部变量 c 并计算 a 和 b 的和
c := a + b
fmt.Printf("a = %d, b = %d, c = %d\n", a, b, c)
}

输出:

1
a = 3, b = 4, c = 7

全局变量

在函数体外声明的变量。全局变量的声明必须以关键字var开头,如果想要在外部包中使用全局变量的首字母必须大写。

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import "fmt"
//声明全局变量
var c int
func main() {
//声明局部变量
var a, b int
//初始化参数
a = 3
b = 4
c = a + b
fmt.Printf("a = %d, b = %d, c = %d\n", a, b, c)
}

输出

1
a = 3, b = 4, c = 7

Go中全局变量和局部变量名称可以相同,但函数体内的局部变量会被优先考虑

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

import "fmt"

//声明全局变量
var a int = 6

func main() {
//声明局部变量
var a int = 3
fmt.Printf("a = %d\n", a)
}

输出

1
a = 3

3. 数据类型

3.1 布尔型

取值只能是true或者false

1
var b bool = true

3.2 数字类型

整型int浮点型float32float64,同时支持复数运算

1
2
3
4
5
6
//浮点数在声明的时候可以只写整数部分或者小数部分
const e = .3652 // 0.3652
const f = 1. // 1
//数值较大或较小可以采用科学计数法, e 或 E 来指定指数部分
const Avogadro = 6.02214129e23 // 阿伏伽德罗常数
const Planck = 6.62606957e-34 // 普朗克常数

计算机中,复数是由两个浮点数表示的,其中一个表示实部(real),一个表示虚部(imag)。

Go语言中复数的类型有两种,分别是 complex128(64 位实数和虚数)和 complex64(32 位实数和虚数),其中 complex128 为复数的默认类型。

声明复数的语法格式

1
2
3
4
5
var z complex128 = complex(x, y)
//等价于
z := complex(x, y)
x = real(z)//获取实部
y = imag(z)//获取虚部

复数也可以用==!=进行相等比较,只有两个复数的实部和虚部都相等的时候它们才是相等的。

3.3 字符类型byte、rune及字符串类型

(a)byte

占用一个字节,8个bit位,表示ASCII表中的一个字符

(b)rune

占用4个字节,32个bit位,表示一个Unicode字符

1
2
3
4
5
6
7
8
9
10
import (
"fmt"
"unsafe"
)

func main() {
var a byte = 'A'
var b rune = 'B'
fmt.Printf("a 占用 %d 个字节数\nb 占用 %d 个字节数", unsafe.Sizeof(a), unsafe.Sizeof(b))
}

输出

1
2
a 占用 1 个字节数
b 占用 4 个字节数

可以看出rune占用的字节数更多,表示的字符范围也比byte更广。

有一点需要注意,与python不同的是,Go中的单引号和双引号并不等价。单引号用来表示字符,双引号用来表示字符串,混用则会导致编译器报错。

(c)字符串

也就是string类型,它的实质就是一堆字符拼接起来的数组。

在初始化字符串时,可以用双引号“”或者反引号。大多情况下,二者并没有区别,但如果你的字符串中有转义字符\ ,这里就要注意了,它们是有区别的。

使用反引号包裹的字符串,会忽略里面的转义。

比如表示 \r\n 这个 字符串,使用双引号是这样写的,这种叫解释型表示法

1
var mystr01 string = "\\r\\n"

而使用反引号,就方便多了,这种叫原生型表示法

1
var mystr02 string = `\r\n`

当你想打印解释型字符串的原型时,可以使用fmt 的 %q 来还原一下。

1
2
3
4
5
6
7
8
9
import (
"fmt"
)

func main() {
var mystr01 string = `\r\n`
fmt.Print(`\r\n`)
fmt.Printf("的解释型字符串是: %q", mystr01)
}

输出如下

1
\r\n的解释型字符串是: "\\r\\n"

在反引号包裹的字符串中,无法用\n表示换行,那么可以直接回车进行换行

1
2
3
4
5
6
7
8
9
import (
"fmt"
)

func main() {
var mystr string = `hello
weipp`
fmt.Println(mystr)
}

输出

1
2
hello
weipp
字符串的常见操作
1.拼接字符串 +

两个字符串 s1 和 s2 可以通过 s := s1 + s2 拼接在一起。将 s2 追加到 s1 尾部并生成一个新的字符串 s。

也可以使用“+=”来对字符串进行拼接:

1
2
3
s := "hel" + "lo,"
s += "world!"
fmt.Println(s) //output “hello, world!”
2.字符串比较
1
2
3
4
 // Compare 函数,用于比较两个字符串的大小,如果两个字符串相等,返回为 0。如果 a 小于 b ,返回 -1 ,反之返回 1 。不推荐使用这个函数,直接使用 == != > < >= <= 等一系列运算符更加直观。
func Compare(a, b string) int
// EqualFold 函数,计算 s 与 t 忽略字母大小写后是否相等。
func EqualFold(s, t string) bool

示例:

1
2
3
4
5
6
7
8
9
10
11
a := "hello weipp"
b := "hello world"
fmt.Println(strings.Compare(a, b)) //-1
fmt.Println(strings.Compare(a, a)) //0
fmt.Println(strings.Compare(b, a)) //1
fmt.Println(a == b) //false
fmt.Println(a >= b) //false
fmt.Println(a <= b) //true 字典序比较

fmt.Println(strings.EqualFold("GO", "go")) //true
fmt.Println(strings.EqualFold("壹", "一")) //false
3.是否存在某个字符或子串

有三个函数能实现相关功能

1
2
3
4
5
6
// 子串 substr 在 s 中,返回 true
func Contains(s, substr string) bool
// chars 中任何一个 Unicode 代码点在 s 中,返回 true
func ContainsAny(s, chars string) bool
// Unicode 代码点 r 在 s 中,返回 true
func ContainsRune(s string, r rune) bool

示例:

1
2
3
4
5
6
7
8
9
10
//1.func Contains(s, substr string) bool
fmt.Println(strings.Contains("abcd", "ab")) //true
//2.func ContainsAny(s, chars string) bool
fmt.Println(strings.ContainsAny("abcd", "e")) //false
fmt.Println(strings.ContainsAny("abcd", "a & c")) //true
fmt.Println(strings.ContainsAny("ab cd", "s g")) //true
fmt.Println(strings.ContainsAny("abcd", "")) //false
fmt.Println(strings.ContainsAny("", "")) //false
//3.func ContainsRune(s string, r rune) bool
fmt.Println(strings.ContainsRune("abcd", 'a')) //true

查看源码可以发现这些函数都是调用了Index函数,Index函数通过子串(字符)与原串的索引进行比较,返回-1,0,1三个数。将这三个数与0比较,返回true或false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Contains reports whether substr is within s.
func Contains(s, substr string) bool {
return Index(s, substr) >= 0
}

// ContainsAny reports whether any Unicode code points in chars are within s.
func ContainsAny(s, chars string) bool {
return IndexAny(s, chars) >= 0
}

// ContainsRune reports whether the Unicode code point r is within s.
func ContainsRune(s string, r rune) bool {
return IndexRune(s, r) >= 0
}
4.大小写转换
1
2
3
4
func ToLower(s string) string
func ToLowerSpecial(c unicode.SpecialCase, s string) string
func ToUpper(s string) string
func ToUpperSpecial(c unicode.SpecialCase, s string) string

大小写转换包含了 4 个相关函数,ToLower,ToUpper 用于大小写转换。ToLowerSpecial,ToUpperSpecial 可以转换特殊字符的大小写。 请看例子:

1
2
3
4
5
6
fmt.Println(strings.ToUpper("hello world"))//HELLO WORLD
fmt.Println(strings.ToUpper("ā á ǎ à"))//Ā Á Ǎ À 汉字拼音有效
fmt.Println(strings.ToUpperSpecial(unicode.TurkishCase, "一"))//一 汉字无效
fmt.Println(strings.ToUpperSpecial(unicode.TurkishCase, "hello world"))//HELLO WORLD
fmt.Println(strings.ToUpper("örnek iş"))//ÖRNEK IŞ
fmt.Println(strings.ToUpperSpecial(unicode.TurkishCase, "örnek iş"))//ÖRNEK İŞ 有细微差别

3.4 派生类型

(a) 指针类型(Pointer)

指针声明格式

1
var var_name *var-type

var-type 为指针类型,var_name 为指针变量名,* 号用于指定变量是作为一个指针。看下示例:

1
2
3
var ip *int        //指向整型
var fp *float32 //指向浮点型
var sp *string //指向字符串

指针创建

1
2
3
4
5
6
7
// 方法1.先定义对应的变量,再通过变量取得内存地址,创建指针
age := 1
ptr := &age

// 方法2.用new创建指针,之后赋值
ptr := new(int)
*ptr = 10

打印指针指向的内存地址

1
2
3
4
5
// 第一种
fmt.Printf("%p", ptr)

// 第二种
fmt.Println(ptr)

Go空指针

当一个指针被定义后没有分配到任何变量时,它的值为 nil。

nil 指针也称为空指针。

nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。

1
2
3
4
5
var  ptr1 *int
fmt.Printf("ptr1 的值为 : %x\n", ptr1 )

ptr2 := new(int)
fmt.Printf("ptr2 的值为 : %x\n", ptr2)

输出:

1
2
ptr1 的值为 : 0
ptr2 的值为 : c000016088

奇怪的事情的发生了,第一种声明方式是空指针,而第二种声明方式显然不是。其实用new方法创建指针的时候,返回值是type初始值的地址,在此例中int的初始值为0,因此返回0值的地址。

可以简单验证下:

1
2
3
4
5
ptr2 := new(int)
fmt.Printf("ptr2 的值为 : %x\n", ptr2)
//ptr2 的值为 : c000016088
fmt.Printf("ptr2 指向的值为 : %x\n", *ptr2)
//ptr2 指向的值为 : 0
(b) 数组类型

数组是具有相同唯一类型的一组长度固定的数据项序列,这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。

数组声明格式

1
var variable_name [SIZE] variable_type

看下示例:

1
var ages [10] int

初始化

1
2
3
4
5
6
7
8
9
10
// 方法1
var ages = [5]int{1,2,3,4,5}
// 方法2
ages := [5]int{1,2,3,4,5}
//数组长度不确定,用...替代元素个数,编译器会自行推断长度并分配空间
var ages = [...]int{1,2,3,4,5}
ages := [...]int{1,2,3,4,5}
//通过指定下标来初始化元素。此时数组长度必须确定。
//初始化索引为1,3的元素
ages := [5]int{1:2,3:4}

[3]int[4]int 虽然都是数组,但他们却是不同的类型,使用 fmt 的 %T 可以查到。

1
2
3
4
5
6
7
8
9
10
import (
"fmt"
)

func main() {
arr01 := [...]int{1, 2, 3}
arr02 := [...]int{1, 2, 3, 4}
fmt.Printf("%d 的类型是: %T\n", arr01, arr01)
fmt.Printf("%d 的类型是: %T", arr02, arr02)
}

输出

1
2
[1 2 3] 的类型是: [3]int
[1 2 3 4] 的类型是: [4]int
(c) 切片类型(slice)

Go 语言切片是对数组的抽象。

事实上切片是一种引用类型,这个片段可以是一整个数组,也可以是数组中的一段,另外,这是左闭右开的区间。

Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片(“动态数组”),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。很像python中的list。

切片的声明和初始化

1.对数组进行片段截取

1
2
3
4
5
6
7
8
9
10
myarr := [5]int{1, 2, 3, 4, 5}
fmt.Printf("myarr 的长度为:%d,容量为:%d\n", len(myarr), cap(myarr))

mysli1 := myarr[1:3]
fmt.Printf("mysli1 的长度为:%d,容量为:%d\n", len(mysli1), cap(mysli1))
fmt.Println(mysli1)

mysli2 := myarr[1:3:4]
fmt.Printf("mysli2 的长度为:%d,容量为:%d\n", len(mysli2), cap(mysli2))
fmt.Println(mysli2)

输出 说明切片的第三个数,影响的只是切片的容量,而不会影响长度

1
2
3
4
5
myarr 的长度为:5,容量为:5
mysli1 的长度为:2,容量为:4
[2 3]
mysli2 的长度为:2,容量为:3
[2 3]

可以发现mysli1和mysli2的打印结果是一样的。那mysli1 := myarr[1:3]mysli2 := myarr[1:3:4]的区别在哪呢

在切片时,若不指定第三个数,那么切片终止索引会一直到原数组的最后一个数。而如果指定了第三个数,那么切片终止索引只会到原数组的该索引值。

好绕是不是,举个例子,mysil := myarr[x:y:z]意思是我将myarr[x,z)的所有值给切片mysil,切片mysil的容量大小为(z-x)。但我打印mysil时,只打印myarr[x,y),切片mysil的长度大小为(y-x)。

代码验证一下

1
2
3
4
5
var numbers4 = [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
myslice := numbers4[4:6:8]
fmt.Printf("myslice为 %d,其容量为:%d,其长度为: %d\n", myslice, cap(myslice), len(myslice))
myslice = myslice[:cap(myslice)]
fmt.Printf("myslice的第四个元素为: %d", myslice[3])

输出

1
2
myslice为 [5 6],其容量为:4,其长度为: 2
myslice的第四个元素为: 8

2.仅声明

1
2
3
4
5
6
7
8
// 声明字符串切片
var strList []string

// 声明整型切片
var numList []int

// 声明一个空切片
var numListEmpty = []int{}

3.make函数构造

make( []Type, size, cap )

我们只需要提供切片所需的三个要素,类型type,长度size和容量cap。

1
2
3
4
5
a := make([]int, 2)
b := make([]int, 2, 10)
fmt.Println(a, b)//[0 0] [0 0]
fmt.Println(len(a), len(b))//2 2
fmt.Println(cap(a), cap(b))//2 10

4.用:=直接声明

1
2
3
a := []int{4:2}
fmt.Println(a)//[0 0 0 0 2]
fmt.Println(len(a), cap(a))//5 5

注意在声明时数组和切片的区别:

数组一定要指明长度,如果没有确切数字也必须要写上...

arr := [5]int{1,2,3,4,5}

arr := [...]int{1,2,3,4,5}

切片长度不固定,不需要指明长度

sli := []int{1,2,3,4,5}

由于切片是引用类型,所以你不对它进行赋值的话,它的零值(默认值)是 nil

1
2
3
var myarr []int
fmt.Println(myarr == nil)
// true

数组和切片都是可以容纳若干类型相同的元素的容器。

不同点在于,数组容器大小固定,而切片作为引用类型,大小不固定,可以在里面随意添加删除元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
    myarr := []int{1}
// 追加一个元素
myarr = append(myarr, 2)
// 追加多个元素
myarr = append(myarr, 3, 4)
// 追加一个切片, ... 表示解包,不能省略
myarr = append(myarr, []int{7, 8}...)
// 在第一个位置插入元素
myarr = append([]int{0}, myarr...)
// 在中间插入一个切片(两个元素)
myarr = append(myarr[:5], append([]int{5,6}, myarr[5:]...)...)
fmt.Println(myarr)
}

输出 如下

1
[0 1 2 3 4 5 6 7 8]
(d) Channel 类型

它是一个数据管道,可以往里面写数据,从里面读数据。

channel 遵循先进先出原则。

写入,读出数据都会加锁,因此线程是安全的。

channel 可以分为 3 种类型:

  • 只读 channel,单向 channel
  • 只写 channel,单向 channel
  • 可读可写 channel

channel 还可按是否带有缓冲区分为:

  • 带缓冲区的 channel,定义了缓冲区大小,可以存储多个数据
  • 不带缓冲区的 channel,只能存一个数据,并且只有当该数据被取出才能存下一个数据

声明和初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 只读 channel
var readOnlyChan <-chan int // channel 的类型为 int

// 只写 channel
var writeOnlyChan chan<- int

// 可读可写
var ch chan int

// 或者使用 make 直接初始化
readOnlyChan1 := make(<-chan int, 2) // 只读且带缓存区的 channel
readOnlyChan2 := make(<-chan int) // 只读且不带缓存区 channel

writeOnlyChan3 := make(chan<- int, 4) // 只写且带缓存区 channel
writeOnlyChan4 := make(chan<- int) // 只写且不带缓存区 channel

ch := make(chan int, 10) // 可读可写且带缓存区

ch <- 20 // 写数据
i := <-ch // 读数据
i, ok := <-ch // 还可以判断读取的数据

基本操作方式

  • 读 <-ch

    i := <-ch

  • 写 ch<-

    ch <- 20

  • 关闭 close(ch)

    close(ch)

带缓冲和不带缓冲的 channel

不带缓冲区 channel

看个例子吧

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() {
ch := make(chan int) // 无缓冲的channel
go unbufferChan(ch)

for i := 0; i < 10; i++ {
fmt.Println("receive ", <-ch) // 读出值
}
}

func unbufferChan(ch chan int) {
for i := 0; i < 10; i++ {
fmt.Println("send ", i)
ch <- i // 写入值
}
}

输出:

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

对于不带缓冲区的channel,只能存一个数据。当读入一个数据后,会加锁,当这个数据被读出时,才会解锁。一个数据的读入和读出一一对应,线程是安全的。那么输出结果为什么是两个send两个receive呢?

我画了一张图讲下我的理解:

首先运行线程1:打印出send 0,将0送入channel,此时lock,之后打印出send 1。再往下无法继续读入,需要读出数据,因此切换至线程2:读出0,打印receive 0。这里解释下为什么线程2读出0后没有直接切换到线程1读入1:因为线程切换需要时间,在准备切换时,已经接到了打印receive 0的命令,此时先打印receive 0,之后再切换到线程1。线程1:将1送入channel,此时函数在改变for循环变量i,这段时间线程2抢到进程。线程2:读出1,打印receive 1。

带缓冲区 channel

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

import (
"fmt"
)

func main() {
ch := make(chan string, 3)
ch <- "tom"
ch <- "jimmy"
ch <- "cate"

fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
}

输出

1
2
3
tom
jimmy
cate

判断 channel 是否关闭

1
v, ok := <-ch

ok 为 true,读到数据,管道没有关闭

ok 为 false,管道已关闭,没有数据可读

浅浅试一下:

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"
)

func main() {
ch := make(chan int)
go test(ch)

for {
if v, ok := <-ch; ok {
fmt.Println("get val: ", v, ok)
} else {
break
}

}
}

func test(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}

输出

1
2
3
4
5
get val:  0 true
get val: 1 true
get val: 2 true
get val: 3 true
get val: 4 true

读已经关闭的 channel 会读到零值,如果不确定 channel 是否关闭,可以用这种方法来检测。

遍历channel

通常用for range遍历channel,,如果发送者没有关闭 channel 或在 range 之后关闭,都会导致 deadlock(死锁)。

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

import "fmt"

func main() {
ch := make(chan int)

go func() {
for i := 0; i < 10; i++ {
ch <- i
}
}()

for val := range ch {
fmt.Println(val)
}
close(ch) // 这里关闭channel已经”通知“不到range了,会触发死锁。
// 不管这里是否关闭channel,都会报死锁,close(ch)的位置就不对。
// 且关闭channel的操作者也错了,只能是发送者关闭channel
}

输出

1
2
3
4
5
6
7
8
9
10
11
0
1
2
3
4
5
6
7
8
9
fatal error: all goroutines are asleep - deadlock!

close(ch) 移到 go func(){}() 里进行修改,像这样:

1
2
3
4
5
6
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}()

select 使用

Go中的select和channel配合使用,通过select可以监听多个channel的I/O读写事件,当 IO操作发生时,触发相应的动作。

基本用法:

1
2
3
4
5
6
7
8
//select基本用法
select {
case <- chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程

看个例子

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

import "fmt"

// https://go.dev/tour/concurrency/5
func fibonacci(ch, quit chan int) {
x, y := 0, 1
for {
select {
case ch <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}

func main() {
ch := make(chan int)
quit := make(chan int)

go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-ch)
}
quit <- 0
}()

fibonacci(ch, quit)
}

输出

1
2
3
4
5
6
7
8
9
10
11
0
1
1
2
3
5
8
13
21
34
quit
(e) 函数类型

函数是基本的代码块,负责执行不同的功能。

Go 语言至少需要有个 main() 函数。

函数声明告诉了编译器函数的名称,返回类型,和参数

函数定义

1
2
3
func function_name( [parameter list] ) [return_types] {
函数体
}
  • func:函数由 func 开始声明
  • function_name:函数名称,参数列表和返回值类型构成了函数签名。
  • parameter list:参数列表
  • return_types:返回类型,函数返回一列值。
  • 函数体:函数定义的代码集合。

函数调用实例

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"

func main() {
/* 定义局部变量 */
var a int = 100
var b int = 200
var ret int

/* 调用函数并返回最大值 */
ret = max(a, b)

fmt.Printf( "最大值是 : %d\n", ret )
}

/* 函数返回两个数的最大值 */
func max(num1, num2 int) int {
/* 定义局部变量 */
var result int

if (num1 > num2) {
result = num1
} else {
result = num2
}
return result
}

输出

1
最大值是 : 200

值传递&引用传递

值传递:值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

引用传递:引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

(f) 结构化类型(struct)

在结构体中我们可以为不同项定义不同的数据类型。

定义结构体

1
2
3
4
5
6
type struct_variable_type struct {
member1 definition1
member2 definition2
...
memberX definitionX
}

结构体变量声明

1
2
3
variable_name := structure_variable_type {value1, value2...valuen}
//或
variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}

看个例子

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

import "fmt"

type Blogs struct {
title string
author string
date string
id int
}

func main() {
// 创建一个新的结构体
fmt.Println(Blogs{"Go入门与实战", "weipp", "23/8/2022", 429761})
// 也可以使用 key => value 格式
fmt.Println(Blogs{title: "Go入门与实战", author: "weipp", date: "23/8/2022", id: 429761})
//忽略的字段为0或空
fmt.Println(Blogs{title: "Go入门与实战", author: "weipp"})
}

输出

1
2
3
{Go入门与实战 weipp 23/8/2022 429761}
{Go入门与实战 weipp 23/8/2022 429761}
{Go入门与实战 weipp 0}

访问结构体成员

  • 结构体.成员名
1
2
3
var blog1 Blogs
blog1.title = "Go入门与实战"
blog1.author = "weipp"
  • 结构体指针.成员名
1
2
3
4
5
6
7
var blog2_ptr *Blogs
//等价于
var blog2 Blogs
blog2_ptr := &blog2

blog2_ptr.title = "Go入门与实战"
blog2_ptr.author = "weipp"
(g) 接口类型(interface)

接口把所有的具有共性的方法定义在一起,其他函数想要使用这些方法时调用接口即可。降低代码的耦合性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 定义接口 */
type interface_name interface {
method_name1 [return_type]
method_name2 [return_type]
method_name3 [return_type]
...
method_namen [return_type]
}
/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
/* 方法实现 */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
/* 方法实现*/
}

示例

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

import (
"fmt"
)

type Phone interface {
call()
}

type NokiaPhone struct {
}

func (nokiaPhone NokiaPhone) call() {
fmt.Println("I am Nokia, I can call you!")
}

type IPhone struct {
}

func (iPhone IPhone) call() {
fmt.Println("I am iPhone, I can call you!")
}

func main() {
var phone Phone

phone = new(NokiaPhone)
phone.call()

phone = new(IPhone)
phone.call()

}
//我们定义了一个接口Phone,接口里面有一个方法call()。然后我们在main函数里面定义了一个Phone类型变量,并分别为之赋值为NokiaPhone和IPhone。然后调用call()方法

输出结果:

1
2
I am Nokia, I can call you!
I am iPhone, I can call you!
(h) Map 类型
  • map是无序的键值对集合,很像python中的字典,是key-value结构。因为是map是用hash表实现的,所以每次打印出来的map都不一样,而且只能通过key获取。
  • map是一种引用类型,长度不固定,和slice一样
  • map的值可以通过重新赋值直接修改

声明和初始化

声明格式

var mapName map[key] value

key为键类型,value为值类型

其中value既可以是基本数据类型,也可以为自定义数据类型

1
2
3
4
5
6
7
8
9
//值类型为int
var numbers map[string] int
//自定义数据类型
var myMap map[string] personInfo
type personInfo struct {
ID string
Name string
Address string
}

初始化

  1. :=直接创建
1
2
rating := map[string] float32 {"C":5, "Go":4.5, "Python":4.5, "C++":2 }
myMap := map[string] personInfo{"1234": personInfo{"1", "Jack", "Room 101,..."},}
  1. make方法构造map

    mapName := make(map[key] value)

1
2
numbers := make(map[string] int)
numbers["one"] = 1

元素查找

这里的查找功能很巧妙,不需要像别的语言那样检查取到的值是否为空,而是返回两个参数。判断第二个参数ok的值0/1即可。

1
2
3
4
value, ok := numbers["one"]
if ok{
//处理找到的value
}

元素修改

非常简单,定位需要修改的key-value对,直接改变value即可

1
numbers["one"] = 11

注意:map是引用类型,如果两个map同时指向一个对象,那么一个改变,另一个也相应改变。

1
2
3
numbersTest := numbers
numbersTest["one"] = "111"
//此时numbers["one"]的值变为"111"了。

元素删除

可以使用Go的内置函数delete(),用于删除容器内的元素。

1
2
//delete(map,key)
delete(number, "one")

上面的代码将从myMap中删除键为“one”的键值对。如果“one”这个键不存在,那么这个调用将什么都不发生,也不会有什么副作用。但是如果传入的map变量的值是nil,该调用将导致程序抛出异常(panic)。

示例

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
50
51
52
53
54
55
56
57
58
59
60
61
62
package main

import "fmt"

type Blogs struct {
title string
author string
date string
id int
}

func main() {
/*
//声明一个map变量numbers,键名为string,值为int
var numbers map[string] int
//给map变量创建值,同时指定最多可以存储3个int值
numbers = make(map[string] int, 3)
//map元素赋值
numbers["one"] = 1
numbers["two"] = 2
numbers["three"] = 3
*/

//上面方式的简写方法
numbers := map[string]int{
"one": 1,
"two": 2,
"three": 3,
}
/*
var myBlogs map[string] Blogs
myBlogs = make(map[string] Blogs)
myBlogs["blog1"] = Blogs{"Day1","weipp","25/8",001}
myBlogs["blog2"] = Blogs{"Day2","weipp","26/8",002}
myBlogs["blog2"] = Blogs{"Day3","weipp","27/8",003}
*/

//上面方式的简写方法
myBlogs := map[string]Blogs{
"blog1": {"Day1", "weipp", "25/8", 001},
"blog2": {"Day2", "weipp", "26/8", 002},
"blog3": {"Day3", "weipp", "27/8", 003},
}

//元素打印
fmt.Println(numbers)
fmt.Println(numbers["two"])
fmt.Println(myBlogs)
fmt.Println(myBlogs["blog1"])

//元素查找
_, ok := myBlogs["blog"]
if ok {
fmt.Println("Found")
} else {
fmt.Println("Not found")
}

//元素删除
delete(numbers, "one")
fmt.Println(numbers)
}

输出

1
2
3
4
5
6
map[one:1 three:3 two:2]
2
map[blog1:{Day1 weipp 25/8 1} blog2:{Day2 weipp 26/8 2} blog3:{Day3 weipp 27/8 3}]
{Day1 weipp 25/8 1}
Not found
map[three:3 two:2]

4.循环控制

4.1条件语句

if-else语句

基本格式

1
2
3
4
5
6
7
8
9
if 条件 1 {
分支 1
} else if 条件 2 {
分支 2
} else if 条件 ... {
分支 ...
} else {
分支 else
}

Go的编译器,对于{}的位置要求十分严格,在编写代码的时候,else ifelse和两边的花括号,必须在同一行,否则编译器报错。

另外需要注意的是,if后面的条件表达式必须严格返回布尔型的数据(0/1/nil均不行)

进阶用法

1
2
3
if 表达式 ; 条件 1 {
分支 1
}

可以先运行一个表达式,再对条件进行判断(这样看起来是不是牛逼点

例如

1
2
3
4
5
6
7
import "fmt"

func main() {
if year := 2020;year >= 2000 {
fmt.Println("现在进入二十一世纪")
}
}
switch 语句

基本格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
switch var0 {
case val1:
...
case val2:
...
default:
...
}
//变量 var 可以是任何类型,而 val1 和 val2 则可以是同类型的任意值。
//还可以同时测试多个可能的值,使用逗号分隔
switch var0 {
case val1,var2,var3:
...
case val4:
...
default:
...
}

switch语句从上到下执行,直到找到匹配。

默认情况下每个case最后都自带break,匹配成功后即跳出循环,不会向下执行。如果需要匹配后面的case,需要加上语句fallthrough

  • switch后的var0是变量
1
2
3
4
5
6
7
8
9
10
	var year int = 2020
switch year {
case 2020:
fmt.Println("Welcome class of 2020!")
case 2021:
fmt.Println("Welcome class of 2021!")
case 2022:
fmt.Println("Welcome class of 2022!")
}
//Welcome class of 2020!
  • switch后的var0是函数

此时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() {
var year int = 2020
switch Welcome(year) {
case true:
fmt.Println("Welcome class of 2020 and after new students!")
case false:
fmt.Println("Welcome old students!")
}
}

func Welcome(year int) bool {
return year >= 2020
}
////Welcome class of 2020!
  • switch后什么都不接

此时相当于 if - elseif - else

1
2
3
4
5
6
7
8
9
10
11
12
13
14
score := 30

switch {
case score >= 95 && score <= 100:
fmt.Println("优秀")
case score >= 80:
fmt.Println("良好")
case score >= 60:
fmt.Println("合格")
case score >= 0:
fmt.Println("不合格")
default:
fmt.Println("输入有误...")
}
  • switch 的 default 不论放在哪都是最后执行
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
//写法1
a := 10

switch {
default : {
fmt.Println("default")
}
case a > 0 : {
fmt.Println("a > 0")
}
case a >5 : {
fmt.Println("a > 5")
}
}
//写法2
a := 10
switch {
case a > 0 : {
fmt.Println("a > 0")
}
case a >5 : {
fmt.Println("a > 5")
}
default : {
fmt.Println("default")
}
}

写法1和写法2没有区别

select 语句

select是一种控制结构,类似于用于通信的switch语句。

select里面的所有case语句要求是对channel操作,无论是在channel中写入数据还是从channel中读出数据。

select随机执行一个可运行的case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。

直接看例子

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

import (
"fmt"
)

func main() {
c1 := make(chan string, 1)
c2 := make(chan string, 1)
c2 <- "hello"
select {
case msg1 := <-c1:
fmt.Println("c1 received: ", msg1)
case msg2 := <-c2:
fmt.Println("c2 received: ", msg2)
}
}

在运行 select 时,会遍历所有(如果有机会的话)的 case 表达式,只要有一个信道有接收到数据,那么 select 就结束,所以输出如下

1
c2 received:  hello

关于deadlock

如果有多个case可以运行,select会随机选出一个执行。其他不会执行。

如果没有一个case可以运行,则会运行default子句。但若没有写default子句,select将阻塞,直到某个通信可以运行。Go 不会重新对 channel 或值进行求值。若一直没有通信能够运行,select就会抛出deadlock错误。如下

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

import (
"fmt"
)

func main() {
c1 := make(chan string, 1)
c2 := make(chan string, 1)

// c2 <- "hello"

select {
case msg1 := <-c1:
fmt.Println("c1 received: ", msg1)
case msg2 := <-c2:
fmt.Println("c2 received: ", msg2)
// default:
// fmt.Println("No data received.")
}
}

输出

1
2
3
4
5
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [select]:
main.main()
d:/my_code/go_code/text/text.go:13 +0xbbexit status 2

如何解决呢?来看一哈

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
//方法1 写defalt 哪怕是空的
c1 := make(chan string, 1)
c2 := make(chan string, 1)

// c2 <- "hello"

select {
case msg1 := <-c1:
fmt.Println("c1 received: ", msg1)
case msg2 := <-c2:
fmt.Println("c2 received: ", msg2)
default:

}
//方法2 让至少一个case可执行
c1 := make(chan string, 1)
c2 := make(chan string, 1)

c2 <- "hello"

select {
case msg1 := <-c1:
fmt.Println("c1 received: ", msg1)
case msg2 := <-c2:
fmt.Println("c2 received: ", msg2)
}

关于随机性和持续执行

select是随机选择case语句,有满足则执行并退出,否则将一直持续检测。随机选择是为了避免饥饿问题(这是网上一个xd说的,有些道理)

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

import (
"fmt" "time")

func Chann(ch chan int, stopCh chan bool) {
for j := 0; j < 10; j++ {
ch <- j
time.Sleep(time.Second)
}
stopCh <- true}

func main() {

ch := make(chan int)
c := 0 stopCh := make(chan bool)

go Chann(ch, stopCh)

for {
select {
case c = <-ch:
fmt.Println("Receive C", c)
case s := <-ch:
fmt.Println("Receive S", s)
case _ = <-stopCh:
goto end
}
}
end:
}

输出

1
2
3
4
5
6
7
8
//第一次输出
Receive C 0
Receive C 1
Receive S 2
//第二次输出
Receive S 0
Receive S 1
Receive S 2

综上,select跟switch有相同点也有不同:

  1. select 只能用于 channel 的操作(写入/读出/关闭),而 switch 则更通用一些,switch后面可以接函数、其他表达式或不接;
  2. select 的 case 是随机的,而 switch 里的 case 是顺序执行;
goto 无条件跳转语句

goto后接一个标签,作用是告诉go程序下一步要执行哪里的代码

goto 用于 跳出多层循环 或者 跳到多层循环的指定层 很好用

easy example

1
2
3
4
    goto flag
fmt.Println("B")
flag:
fmt.Println("A")

执行结果,并不会输出 B ,而只会输出 A

1
A

注意:goto语句与标签之间不能有变量声明,否则编译错误。

defer 延迟执行

未完待续…

参考