本文的内容基于 Go 语言简明教程, 目的是快速学习 Go 的基本用法。

Variables and Built-in Data Types

Variable

Go 作为静态类型的语言,变量声明的时候必须要明确变量的类型。 Go 语言的类型写在变量后面。

1
2
3
4
5
var a int // 没有赋值,默认为 0
var a int = 1 // 声明时进行赋值
var a = 1 // 声明时赋值, 1 是 int 了欸行,在赋值时,a 被自动确定为 int 类型
a := 1 // 更简洁的写法
msg := "Hello World!"

简单类型

1
2
3
4
5
6
nil // 不是保留字,只能赋值给指针、channel、func、interface、map、slice
var a int8 = 10 // int, int8, int16, int32, unit8, uint16
var c1 byte = 'a' // byte 等价于 uint8
var b float32 = 12.2 // float32, float64
var msg = "Hellow World!" // String
ok := false // boolean (true or false)

字符串

golang 字符串的底层是一个 byte 数组,unint8 的数组

utf8 进行存储,一个英文字母占 1 个 byte,一个中文字符占 3 个 byte。

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

import (
"fmt"
"reflect"
)

func main() {
str1 := "Golang"
str2 := "Go语言"
fmt.Println(reflect.TypeOf(str2[2]).Kind()) // uint8 字符串以 unit8(byte) 数组形式保存,打印时需要用 string 进行类型转换。 否则打印编码值
fmt.Println(str1[2], string(str1[2])) // 108 l
fmt.Printf("%d %c\n", str2[2], str2[2]) // 232 è 看到的并不是 “语", 因为实际上是取了 uint8 数组中的 index = 2 的内容。
fmt.Println("len(str2):", len(str2)) // 8 GO:2 语言:6
}

从以上代码中可以看到,由于 go 中一个中文字符占 3 个 byte,导致使用 string 数组不能正确切片,所以正确的处理方式是将 string 转为 rune 数组。rune 是 int32 的别名。使用[]int32 进行类型转换也是一样的效果。

1
2
3
4
runeArr := []rune(str2)
fmt.Println(reflect.TypeOf(runeArr[2]).Kind()) // int32
fmt.Println(runeArr[2], string(runeArr[2])) // 35821 语
fmt.Println("len(runeArr):", len(runeArr)) // len(runeArr): 4

Array and Slice

Array

1
2
3
4
5
6
7
8
9
array and slice
var arr [5]int // one dimention
var arr2 [5][5]int // two dimentions
arr := [5]int{1, 2, 3, 4, 5} // var arr = [5]int {1,2,3...}
for i := 0; i < len(arr); i++ {
arr[i] += 100
}
fmt.Println(arr)

数组的长度不能改变,如若想要拼接或者获取子数组,需要使用切片。切片是使用数组作为底层结构的抽象。切片包含三个组件:容量,长度和指向底层数组的指针,切片可以随时进行扩展。

Slice

声明切片

1
2
3
slice1 := make([]float32, 0)
slice2 := make([]float32, 3, 5) // 长度为 3,容量为 5 的切片
fmt.Println(len(slice1), cap(slice2)) // 0, 5

使用切片

1
2
3
4
5
6
7
8
9
10
11
12
// 添加元素,切片容量可以根据需要自动扩展
slice2 = append(slice2, 1, 2, 3, 4) // [0, 0, 0, 1, 2, 3, 4]
fmt.Println(len(slice2), cap(slice2)) // 7 12
// 子切片 [start, end)
sub1 := slice2[3:] // [1 2 3 4]
sub2 := slice2[:3] // [0 0 0]
//sub3 := slice2[1:4] // [0 0 1]
// 合并切片

combined := append(sub1, sub2...) // [1, 2, 3, 4, 0, 0, 0]
// sub2... 是切片结构的写法,将切片解构为 N 个独立的元素
fmt.Println(combined)

申明切片的时候会需要设置容量大小,为切片预分配空间,实际使用过程中,如果容量不够,切片容量会自动扩展。

Map

1
2
3
4
5
6
7
8
9
10
11
// 仅声明
m1 := make(map[string]int)
// 声明时初始化
m2 := map[string]string{
"Sam": "Male",
"Alice": "Female",
}
// 赋值/修改
m1["Tom"] = 18
fmt.Println(m1) // map[Tom:18]
fmt.Println(m2) // map[Alice:Female Sam:Male]

Pointer

使用 * 定义指针,使用 & 获取某个变量的地址

1
2
3
4
5
6
var n int = 1
add(n) // 值传递
fmt.Println(n) // 1

realAdd(&n) // 参数传递
fmt.Println(n) // 2

Control Flow (if, for, switch)

if else

1
2
3
4
5
6
7
8
9
10
11
12
13
age := 17
if age < 18 {
fmt.Printf("Kid\n")
} else {
fmt.Printf("Adult")
}

// 简写方式
if age := 18; age < 18 {
fmt.Printf("Kid")
} else {
fmt.Printf("Adult\n")
}

switch

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
type Gender int8 // type 关键字定义新的类型 Gender
const (
MALE Gender = 1 // 是以哦那个 const 定义了 MALE FEMALE 2 个常量,GO 中没有枚举类型的概念,使用常量的方式来模拟枚举
FEMALE Gender = 2
)

gender := MALE

switch gender {
case FEMALE:
fmt.Println("female") // go de switch 不需要 break,匹配到某个 case 就默认不会继续放下执行
case MALE:
fmt.Println("male")
default:
fmt.Println("unkonwn")
} // male

switch gender {
case FEMALE:
fmt.Println("female")
fallthrough // 如果需要继续执行,需要使用 fallthrough
case MALE:
fmt.Println("male")
fallthrough
default:
fmt.Println("unkonwn")
} // male unknown

for loop

for loop,c++ 写法和 python 写法

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
// for loop
sum := 0
for i := 0; i < 10; i++ {
if sum > 20 {
break
}
sum += i
fmt.Println(sum)
}
// 0
// 1
// 3
// 6
// 10
// 15
// 21

nums := []int{1, 2, 3, 4, 5}
for i, num := range nums {
fmt.Println(i, num)
}
// 0 1
// 1 2
// 2 3
// 3 4
// 4 5
m2 := map[string]string{
"Jhon": "MALE",
"Alice": "FEMALE", // comma should be added in the last key-value pair
}

for key, value := range m2 {
fmt.Println(key, value)
}
// Jhon MALE
// Alice FEMALE

Functions

Parameter and Return Values

对于一个 go 中的典型函数,参数可以有多个,返回值也可以有多个。

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

func div2(num1 int, num2 int)( quo int, rem int){ // 除了声明类型,还可以在函数定义的时候声明返回值,进行命名。
quo = num1 / num2
rem = num1 % num2
return
//return num1 / num2, num1 % num2
}


func main() {
quo, rem := div(100, 17)
fmt.Println(quo, rem)

quo1, rem1 := div(100, 17)
fmt.Println(quo1, rem1) // 5, 15
}

Error Handling

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

func main(){
_, err := os.Open("noexist.txt") // *file, err
if err != nil {
fmt.Println(err) // open noexist.txt: no such file or directory
}
}

使用 errorw.New 返回自定义的错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import (
"fmt"
"os"
"error"
)

func hello(name string) error {
if len(name) == 0 {
return errors.New("error: name is null")
}
fmt.Println("Hello", name)
return nil
}

func main(){
if err := hello(""); err != nil { // assignment in the condition
fmt.Println(err) // error: name is null
}
}

出现一些不可预知的错误,比如数组越界,这种错误可能会导致程序非正常退出,在 Go 语言中称之为 panic

1
2
3
4
5
6
7
8
9
func get(index int) int {
arr := [3]int{2, 3, 4}
return arr[index]
}

func main() {
fmt.Println(get(5))
fmt.Println("finished")
}

报错

1
2
3
4
5
6
7
8
anic: runtime error: index out of range [5] with length 3

goroutine 1 [running]:
main.get(...)
/home/yiwei/go_test/main.go:38
main.main()
/home/yiwei/go_test/main.go:203 +0x1d
exit status 2

在 Python 和 Java 等语言中,有 try … catch 机制, Go 中类似的机制是 defer,recover

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func get(index int) (ret int) {
defer func() {
if r := recover(); r != nil {
fmt.Println("Some Error !", r)
ret = -1
}
}() // defer 定义了异常处理的函数,在协程推出之前,会执行 defer 挂载的任务
// 如果触发了 panic,控制权就交给了 defer
// 使用 recover() 是的程序恢复正常,并且将返回值设置为 -1
// 如果不设置,默认返回为 0
arr := [3]int{2, 3, 4}
return arr[index]
}

func main() {
fmt.Println(get(5))
fmt.Println("finished")
}

Struct, Method and Interface

Struct and Method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Student struct {
name string
age int
}

// 方法和函数的区别是, func 和函数名 hello 之间,会加上该方法对应的实例名 student 及类型

func (student *Student) hello(person string) string {
return fmt.Sprintf("hello %s, I am %s", person, student.name)
}

func main() {
stu := &Student{
name: "Tom", // 并不是每个字段都需要被赋值,没有显性赋值的变量将是默认值
}

msg := stu.hello("Jack")
fmt.Println(msg) // hello Jack, I am Tom

stu1 := new(Student) //使用 new 进行实例化
fmt.Println(stu1.hello("Alice")) // hello Alice, I am ,name 是默认值

}

Interfaces

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 Person interface {
getName() string
getAge() int
}

type Student struct {
name string
age int
}

// getName 接口1
func (stu *Student) getName() string {
return stu.name
}

func (stu *Student) getAge() int {
return stu.age
}

type Worker struct {
name string
gender string
}

// 接口2
func (w *Worker) getName() string {
return w.name
}


func main() {
var p Person = &Student{ // 实例化之后,强制类型转换为接口类型 Person,如果 Student 没有完全实现 Person 的方法,编译时将会出现某写方法没有 implement 的报错
name: "Tom",
age: 18,
}
fmt.Println(p.getName()) // Tom

stu := p.(*Student) // 将接口转换为实例
fmt.Println(stu.getAge()) // 18
}

测试某个类型实现了某个接口的全部方法

1
2
var _ Person = (*Student)(nil)
var _ Person = (*Worker)(nil) // 如果实现不完整,编译期就会报错

如果一个没有任何方法的空接口,可以表示任意类型

1
2
3
4
5
6
7
8
func main() {
// 构建了一个类似 python dict 可以容纳任何类型的 map
m := make(map[string]interface{})
m["name"] = "Tom"
m["age"] = 18
m["scores"] = [3]int{98, 99, 85}
fmt.Println(m) // map[age:18 name:Tom scores:[98 99 85]]
}

Goroutines

Goroutines are functions or methods that run concurrently with other functions and method. Goroutines can be thought as light weight threads.

Sync

Go 提供了 sync 和 channel 两种方式支持 Goroutine 的并发

如果我们希望并发下载 N 个资源,且多个并发写成之间不需要通信,就可以使用 sync.WaitGroup, 等待所有并发协程结束

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

var wg sync.WaitGroup

func download(url string) {
fmt.Println("start to download", url)
time.Sleep(time.Second) //模拟耗时操作
wg.Done() //减少一个计数器
}

func main() {
for i := 0; i < 3; i++ {
wg.Add(1) // 增加一个计数器
go download("a.com/" + string(i+'0')) //启动新的写成并发执行 download 函数
}

wg.Wait() // 等待所有的协程执行结束
fmt.Println("Done!")
}

Channel

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

//package main

import (
"fmt"
"time"
)

// 使用 channel 信道,可以在协程之间传递消息,组测等待并发协程返回消息
var ch = make(chan string, 10) // 创建大小为 10 的缓冲信道

func download(url string) {
fmt.Println("start to download", url)
time.Sleep(time.Second)
ch <- url // 将 url 消息传递至信道中
}

func main() {
for i := 0; i < 3; i++ {
go download("a.com/" + string(i+'0'))
}
for i := 0; i < 3; i++ {
msg := <-ch // 等待信道返回消息
fmt.Println("finish", msg)
}
fmt.Println("Done!")
}

1
2
3
4
5
6
7
start to download a.com/2
start to download a.com/1
start to download a.com/0
finish a.com/1
finish a.com/2
finish a.com/0
Done!

Unit Test

calc.go

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

import "testing"

func TestAdd(t *testing.T) {
if ans := add(1, 2); ans != 3 {
t.Error("add(1,2) should be equal to 3")
}
}

calc_test.go

1
2
3
4
5
package main

func add(num1 int, num2 int) int {
return num1 + num2
}

go test calc_test.go calc.go -v 运行命令行,将两个文件连起来并进行单元测试

1
2
3
4
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok command-line-arguments 0.002s

Package and Module

Package

一般来说,一个文件夹可以作为一个 package ,同一个 package 对的内部变量、类型、方法等定义可以相互看到。

Module

Go Mdules 是 Go 1.11 版本之后引入的。Go Modules 是较为完善的包管理工具。Go Modules 在 1.13 版本仍然是可选使用的, GO111MODULE 的默认值为 AUTO,如果需要强制使用 Go Modules 进行以来管理,可以将其设置成 ON。

文件及其相关内容如下所示:

1
2
3
4
demo/
|--calc/
|--calc.go
|--main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"example/calc"
"fmt"

"rsc.io/quote"
)

func main() {
fmt.Println(calc.Add(1, 2)) // 3
fmt.Println(quote.Hello()) // Ahoy, world!
}

1
2
3
4
5
package calc

func Add(num1 int, num2 int) int {
return num1 + num2
}
1
2
3
4
5
6
7
8
9
10
$ go mod init example
go: creating new go.mod: module example
go: to add module requirements and sums:
go mod tidy
$ go mod tidy
go: finding module for package rsc.io/quote
go: found rsc.io/quote in rsc.io/quote v1.5.2
$ go run .
3
Ahoy, world!