The tour of go
应原生态coroutine需求!
代码被分组到包中(即包收集相关函数),包被分组到模块中
go mod init <file>
创建go.mod
来跟踪代码的依赖项,使用go mod init
命令来创建新模块go mod init example.com/myproject
将会在当前目录下初始化名为example.com/project
的模块
名称以大写字母开始的函数可以由不在同一包中的函数调用
:=
运算会自动推导右侧变量类型。1
2
3
4message := 1
// 等价于
var message int
message = 1go run .
会编译并运行当前目录下的go文件;go build .
会编译当前目录下的go文件并生成可执行文件。go mod tidy
用于管理go的依赖项,根据源项目代码中实际引用的依赖项,自动更新go.mod文件的依赖列表,以及删除未引用的依赖go的模块命名`
/ $ go mod edit -replace example.com/greetings=../greetings
,将模块重定向到本地路径go中连续类型相同的参数,除了最后一个类型以外都可以省略类型名
x int, y int
,x, y int
使用var声明变量,类型在最后
var i, j int = 1, 2
,可以用这种方式声明变量go中只允许显式类型转换,如int(a)
常量不能用
:=
声明(因为:=
涉及类型推导,常量可以是字符、字符串、布尔值、数值)基本类型包括
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // uint8 的别名
rune // int32 的别名
// 表示一个 Unicode 码点
float32 float64
complex64 complex128for语句,go中三个构成外面没有小括号,if也一样
1
2
3
4// go中只有后置++
for i := 0; i < 10; i++ {
sum += i
}for也可以充当c中的关键字while, go中没有while
1
2
3
4
5
6
7for sum < 1000 {
sum += sum
}
// 无限循环
for {
}if可以在条件判断前执行一个简单的语句,该语句声明的变量作用域在if内
1
2
3if v := math.Pow(x, n); v < lim {
return v
}switch语句,go会在每个case后自动加一个break,go中switch的case无需为常量,且取值不必为整数,也可以在判断条件之前加入一个简单的语句
1
2
3
4
5
6
7
8
9
10switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}for中的无限循环和switch true一样,默认不写条件就代表true,更加清晰
1
2
3
4
5
6
7
8switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}defer语句会函数延迟到作用域结束后执行,但其参数会立即求值!
defer语句会将函数压入栈中,返回时按照弹栈的顺序调用执行
struct结构体要用type标识
1
2
3
4
5
6
7
8
9type Vertex struct {
X int
Y int
}
// 初始化结构体对象
v := Vertex{1, 2}
v2 = Vertex{X: 1} // Y:0 被隐式地赋予
v3 = Vertex{} // X:0 Y:0
v.X = 4数组,**[n]T表示拥有n个T类型的值的数组。声明时括号在前**
1
2
3var a [2]string
a[1] = "hello" // 元素赋值的方式初始化数组元素
primes := [6]int{2, 3, 5, 7, 11, 13} // 列表方式初始化数组元素切片(没有长度的数组),**[]T表示一个元素类型为T的切片**。切片的零值是
nil
(长度和容量为0且没有底层数组)1
2
3
4
5
6
7
8
9
10var b [3]string
// c为切片类型,将b的最后两个元素赋给c
var c []string = b[1:2]
var a [10]int
// 以下切片是等价的
a[0:10]
a[:10]
a[0:]
a[:]- 可以通过len(s),和cap(s)来获取切片的长度和容量。类似于C++vector的长度和容量,即size()和capacity(),同样有扩容机制
- 切片可以用
make
来创建1
2a := make([]int, 5) // 长度为5
b := make([]int, 0, 5) // 长度为0,容量为5 - 切片可以用
append
方法添加元素1
2
3
4
5
6// 添加一个空切片
s = append(s, 0)
// 这个切片会按需增长
s = append(s, 1)
// 可以一次性添加多个元素
s = append(s, 2, 3, 4)
for范围循环,遍历切片时,每次迭代都会返回两个值<下标,值的副本>,关键字有
range
1
2
3
4
5
6
7
8
9
10var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
// 只获取索引
for i := range pow {}
// 可以将下标赋予"_"来忽略它(防止编译器报错)
for i, _ := range pow {}map,声明
hash := make(map[Keytype]ValueType)
,要使用make来创建1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25type Vertex struct {
Lat, Long float64
}
var m = map[string]Vertex{
"Bell Labs": Vertex{
40.68433, -74.39967,
},
"Google": Vertex{
37.42202, -122.08408,
},
}
// 初始化map对象m,键类型为string,值为int
m := make(map[string]int)
// 可以用map[type]bool代表set,它是无序的
// 插入键值对
m["Answer"] = 42
// 修改键的值
m["Answer"] = 48
// 删除键值对
delete(m, "Answer")
// 返回的ok是bool类型,用于检测某个键是否存在
v, ok := m["Answer"]strings.Fields()
函数用于将字符串按照空格分割成一个字符串切片(即由多个子串组成的序列),其中每个子串都是一个单词1
2
3
4
5
6
7
8
9
10// 统计单词次数
func WordCount(s string) map[string]int {
hash := make(map[string]int)
// 将字符串按空格分割成单词
words := strings.Fields(s)
for _, ch := range words {
hash[ch]++
}
return hash
}
函数指针,其类型需要加上参数类型和返回类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 可以注意到传入的函数指针变量是带有,其函数的返回类型和参数类型的
func compute(fn func(float64, float64) float64) float64 {
// 调用函数指针变量fn
return fn(3, 4)
}
func main() {
// hypot为函数指针变量
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(5, 12))
fmt.Println(compute(hypot))
fmt.Println(compute(math.Pow))
}lambda表达式
1
2
3
4
5
6
7
8func adder() func(int) int {
sum := 0
// 返回为lambda表达式(即作为一个函数指针变量)
return func(x int) int {
sum += x
return sum
}
}go中没有类
go中的方法(即成员函数), 即在func关键字和方法名之间放入方法接收者(即调用者) ;函数返回为空不需要写返回类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21type Vertex struct {
X, Y float64
}
// 方法
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
// 函数
func Abs(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := Vertex{3, 4}
// 调用方法
v.Abs()
// 调用函数
Abs(v)
}go中要想实现像cpp那样传引用或指针修改值,总的说要想传指针,取决于参数,go语言会自动这里帮助推导类型,省去很多麻烦
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 注意指针的声明
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
v := Vertex{3, 4}
// 方法调用时也不需要写成->
// 调用者既能作为值又能作为指针
v.Scale(10) // 会被解释为(&v).Scale(10)
// 或者
func (v Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
s := &Vertex{2, 3}
s.Scale(10) // 会被解释为(*s).Scale(10)接口
- 接口是一组方法签名定义的集合,将对应的方法放进接口中,即方法接收者后面的方法名和返回类型,然后将调用者赋给接口变量,这时候就可以利用接口变量调用对应的方法(但注意这时候调用者没有像上面那样的推导,得传入对应的值或指针)
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
37type Abser interface {
Abs() float64
}
func main() {
// 接口变量
var a Abser
f := MyFloat(-math.Sqrt2)
v := Vertex{3, 4}
a = f // a MyFloat 实现了 Abser
a = &v // a *Vertex 实现了 Abser
// 下面一行,v 是一个 Vertex(而不是 *Vertex),没有推导
// 所以没有实现 Abser。
a = v
// 根据接口变量来调用对应版本的方法
fmt.Println(a.Abs())
}
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
type Vertex struct {
X, Y float64
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
} - 可以利用格式符
%v
和%T
来获得接口变量的值和返回类型 - 如果接口变量没有指定具体的调用者,即不能指明具体调用哪个方法,则会返回nil运行时错误
- 空接口,可以保存任何类型的值,接口也是值!
1
interface{}
- 使用
类型断言
t:= i.(T)可以获得接口的值,以及是否保存该特定类型的bool变量1
2
3
4
5
6
7
8
9
10var i interface{} = "hello"
s := i.(string)
fmt.Println(s)
s, ok := i.(string)
fmt.Println(s, ok)
f, ok := i.(float64)
fmt.Println(f, ok) // ok为false,f将为float64类型的零值,不会产生panic - i.(type),使用关键字type可以获得当前接口变量的类型
- fmt包通过Stinger接口来打印值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// type Stringer interface {
// String() string
// }
type Person struct {
Name string
Age int
}
// 调用者为Person
func (p Person) String() string {
return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}
func main() {
a := Person{"Arthur Dent", 42}
z := Person{"Zaphod Beeblebrox", 9001}
fmt.Println(a, z)
}
- 接口是一组方法签名定义的集合,将对应的方法放进接口中,即方法接收者后面的方法名和返回类型,然后将调用者赋给接口变量,这时候就可以利用接口变量调用对应的方法(但注意这时候调用者没有像上面那样的推导,得传入对应的值或指针)
go协程
1
2// 新建一个协程并执行该函数
go f(x, y, z)channel,类似于管道,没有数据传输会阻塞,也可以指定带缓冲的管道的缓冲大小
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 将和送入 c
}
func main() {
// 新建一个数组
s := []int{7, 2, 8, -9, 4, 0}
// 使用chan新建传输int类型的channel
c := make(chan int)
// 带缓冲的channel,缓冲区填满之后再往里面填会阻塞(这时候会报错)
ch := make(chan int, 100)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 从 c 中接收
fmt.Println(x, y, x+y)
}go中可以直接使用IO多路复用select,就像用switch一样舒服,如果没有任务就会阻塞(当然也可以设置以下default,channel上没有信号的时候就执行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
27func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
// 利用IO多路复用,等待channel上的信号
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
defualt:
XXX
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}