Go 语言简明教程 | 快速入门 | 极客兔兔 - 图1

Go(又称 Golang)是 Google 开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。 —— Go - wikipedia.org

1 Go 安装

最新版本下载地址官方下载 golang.org,当前是 1.13.6。如无法访问,可以在 studygolang.com/dl 下载

使用 Linux,可以用如下方式快速安装。

  1. $ wget https://studygolang.com/dl/golang/go1.13.6.linux-amd64.tar.gz
  2. $ tar -zxvf go1.13.6.linux-amd64.tar.gz
  3. $ sudo mv go /usr/local/
  4. $ go version
  5. go version go1.13.6 linux/amd64

Go 1.11 版本开始,Go 提供了 Go Modules 的机制,推荐设置以下环境变量,第三方包的下载将通过国内镜像,避免出现官方网址被屏蔽的问题。

  1. $ go env -w GOPROXY=https://goproxy.cn,direct

或在 ~/.profile 中设置环境变量

  1. export GOPROXY=https://goproxy.cn

2 Hello World

新建一个文件 main.go,写入

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

执行go run main.gogo run .,将会输出

  1. $ go run .
  2. Hello World!

如果强制启用了 Go Modules 机制,即环境变量中设置了 GO111MODULE=on,则需要先初始化模块 go mod init hello
否则会报错误:go: cannot find main module; see ‘go help modules’

我们的第一个 Go 程序就完成了,接下来我们逐行来解读这个程序:

  • package main:声明了 main.go 所在的包,Go 语言中使用包来组织代码。一般一个文件夹即一个包,包内可以暴露类型或方法供其他包使用。
  • import “fmt”:fmt 是 Go 语言的一个标准库 / 包,用来处理标准输入输出。
  • func main:main 函数是整个程序的入口,main 函数所在的包名也必须为 main
  • fmt.Println(“Hello World!”):调用 fmt 包的 Println 方法,打印出 “Hello World!”

go run main.go,其实是 2 步:

  • go build main.go:编译成二进制可执行程序
  • ./main:执行该程序

3 变量与内置数据类型

3.1 变量 (Variable)

Go 语言是静态类型的,变量声明时必须明确变量的类型。Go 语言与其他语言显著不同的一个地方在于,Go 语言的类型在变量后面。比如 java 中,声明一个整体一般写成 int a = 1,在 Go 语言中,需要这么写:

  1. var a int
  2. var a int = 1
  3. var a = 1

var a = 1,因为 1 是 int 类型的,所以赋值时,a 自动被确定为 int 类型,所以类型名可以省略不写,这种方式还有一种更简单的表达:

  1. a := 1
  2. msg := "Hello World!"

3.2 简单类型

空值:nil

整型类型: int(取决于操作系统), int8, int16, int32, int64, uint8, uint16, …

浮点数类型:float32, float64

字节类型:byte (等价于 uint8)

字符串类型:string

布尔值类型:boolean,(true 或 false)

  1. var a int8 = 10
  2. var c1 byte = 'a'
  3. var b float32 = 12.2
  4. var msg = "Hello World"
  5. ok := false

3.3 字符串

在 Go 语言中,字符串使用 UTF8 编码,UTF8 的好处在于,如果基本是英文,每个字符占 1 byte,和 ASCII 编码是一样的,非常节省空间,如果是中文,一般占 3 字节。包含中文的字符串的处理方式与纯 ASCII 码构成的字符串有点区别。

我们看下面的例子:

  1. package main
  2. import (
  3. "fmt"
  4. "reflect"
  5. )
  6. func main() {
  7. str1 := "Golang"
  8. str2 := "Go语言"
  9. fmt.Println(reflect.TypeOf(str2[2]).Kind())
  10. fmt.Println(str1[2], string(str1[2]))
  11. fmt.Printf("%d %c\n", str2[2], str2[2])
  12. fmt.Println("len(str2):", len(str2))
  13. }
  • reflect.TypeOf().Kind() 可以知道某个变量的类型,我们可以看到,字符串是以 byte 数组形式保存的,类型是 uint8,占 1 个 byte,打印时需要用 string 进行类型转换,否则打印的是编码值。
  • 因为字符串是以 byte 数组的形式存储的,所以,str2[2] 的值并不等于。str2 的长度 len(str2) 也不是 4,而是 8( Go 占 2 byte,语言占 6 byte)。

正确的处理方式是将 string 转为 rune 数组

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

转换成 []rune 类型后,字符串中的每个字符,无论占多少个字节都用 int32 来表示,因而可以正确处理中文。

3.4 数组 (array) 与切片(slice)

声明数组

  1. var arr [5]int
  2. var arr2 [5][5]int

声明时初始化

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

使用 [] 索引 / 修改数组

  1. arr := [5]int{1, 2, 3, 4, 5}
  2. for i := 0; i < len(arr); i++ {
  3. arr[i] += 100
  4. }
  5. fmt.Println(arr)

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

声明切片:

  1. slice1 := make([]float32, 0)
  2. slice2 := make([]float32, 3, 5)
  3. fmt.Println(len(slice2), cap(slice2))

使用切片:

  1. slice2 = append(slice2, 1, 2, 3, 4)
  2. fmt.Println(len(slice2), cap(slice2))
  3. sub1 := slice2[3:]
  4. sub2 := slice2[:3]
  5. sub3 := slice2[1:4]
  6. combined := append(sub1, sub2...)
  • 声明切片时可以为切片设置容量大小,为切片预分配空间。在实际使用的过程中,如果容量不够,切片容量会自动扩展。
  • sub2... 是切片解构的写法,将切片解构为 N 个独立的元素。

3.5 字典 (键值对,map)

map 类似于 java 的 HashMap,Python 的字典 (dict),是一种存储键值对(Key-Value) 的数据解构。使用方式和其他语言几乎没有区别。

  1. m1 := make(map[string]int)
  2. m2 := map[string]string{
  3. "Sam": "Male",
  4. "Alice": "Female",
  5. }
  6. m1["Tom"] = 18

3.6 指针 (pointer)

指针即某个值的地址,类型定义时使用符号*,对一个已经存在的变量,使用 & 获取该变量的地址。

  1. str := "Golang"
  2. var p *string = &str
  3. *p = "Hello"
  4. fmt.Println(str)

一般来说,指针通常在函数传递参数,或者给某个类型定义新的方法时使用。Go 语言中,参数是按值传递的,如果不使用指针,函数内部将会拷贝一份参数的副本,对参数的修改并不会影响到外部变量的值。如果参数使用指针,对参数的传递将会影响到外部变量。

例如:

  1. func add(num int) {
  2. num += 1
  3. }
  4. func realAdd(num *int) {
  5. *num += 1
  6. }
  7. func main() {
  8. num := 100
  9. add(num)
  10. fmt.Println(num)
  11. realAdd(&num)
  12. fmt.Println(num)
  13. }

4 流程控制 (if, for, switch)

4.1 条件语句 if else

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

4.2 switch

  1. type Gender int8
  2. const (
  3. MALE Gender = 1
  4. FEMALE Gender = 2
  5. )
  6. gender := MALE
  7. switch gender {
  8. case FEMALE:
  9. fmt.Println("female")
  10. case MALE:
  11. fmt.Println("male")
  12. default:
  13. fmt.Println("unknown")
  14. }
  • 在这里,使用了type 关键字定义了一个新的类型 Gender。
  • 使用 const 定义了 MALE 和 FEMALE 2 个常量,Go 语言中没有枚举 (enum) 的概念,一般可以用常量的方式来模拟枚举。
  • 和其他语言不同的地方在于,Go 语言的 switch 不需要 break,匹配到某个 case,执行完该 case 定义的行为后,默认不会继续往下执行。如果需要继续往下执行,需要使用 fallthrough,例如:
  1. switch gender {
  2. case FEMALE:
  3. fmt.Println("female")
  4. fallthrough
  5. case MALE:
  6. fmt.Println("male")
  7. fallthrough
  8. default:
  9. fmt.Println("unknown")
  10. }

4.3 for 循环

一个简单的累加的例子,break 和 continue 的用法与其他语言没有区别。

  1. sum := 0
  2. for i := 0; i < 10; i++ {
  3. if sum > 50 {
  4. break
  5. }
  6. sum += i
  7. }

对数组 (arr)、切片 (slice)、字典 (map) 使用 for range 遍历:

  1. nums := []int{10, 20, 30, 40}
  2. for i, num := range nums {
  3. fmt.Println(i, num)
  4. }
  5. m2 := map[string]string{
  6. "Sam": "Male",
  7. "Alice": "Female",
  8. }
  9. for key, value := range m2 {
  10. fmt.Println(key, value)
  11. }

5 函数 (functions)

5.1 参数与返回值

一个典型的函数定义如下,使用关键字 func,参数可以有多个,返回值也支持有多个。特别地,package main 中的func main() 约定为可执行程序的入口。

  1. func funcName(param1 Type1, param2 Type2, ...) (return1 Type3, ...) {
  2. }

例如,实现 2 个数的加法(一个返回值)和除法(多个返回值):

  1. func add(num1 int, num2 int) int {
  2. return num1 + num2
  3. }
  4. func div(num1 int, num2 int) (int, int) {
  5. return num1 / num2, num1 % num2
  6. }
  7. func main() {
  8. quo, rem := div(100, 17)
  9. fmt.Println(quo, rem)
  10. fmt.Println(add(100, 17))
  11. }

也可以给返回值命名,简化 return,例如 add 函数可以改写为

  1. func add(num1 int, num2 int) (ans int) {
  2. ans = num1 + num2
  3. return
  4. }

5.2 错误处理 (error handling)

如果函数实现过程中,如果出现不能处理的错误,可以返回给调用者处理。比如我们调用标准库函数os.Open读取文件,os.Open 有 2 个返回值,第一个是 *File,第二个是 error, 如果调用成功,error 的值是 nil,如果调用失败,例如文件不存在,我们可以通过 error 知道具体的错误信息。

  1. import (
  2. "fmt"
  3. "os"
  4. )
  5. func main() {
  6. _, err := os.Open("filename.txt")
  7. if err != nil {
  8. fmt.Println(err)
  9. }
  10. }

可以通过 errorw.New 返回自定义的错误

  1. import (
  2. "errors"
  3. "fmt"
  4. )
  5. func hello(name string) error {
  6. if len(name) == 0 {
  7. return errors.New("error: name is null")
  8. }
  9. fmt.Println("Hello,", name)
  10. return nil
  11. }
  12. func main() {
  13. if err := hello(""); err != nil {
  14. fmt.Println(err)
  15. }
  16. }

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

  1. func get(index int) int {
  2. arr := [3]int{2, 3, 4}
  3. return arr[index]
  4. }
  5. func main() {
  6. fmt.Println(get(5))
  7. fmt.Println("finished")
  8. }
  1. $ go run .
  2. panic: runtime error: index out of range [5] with length 3
  3. goroutine 1 [running]:
  4. exit status 2

在 Python、Java 等语言中有 try...catch 机制,在 try 中捕获各种类型的异常,在 catch 中定义异常处理的行为。Go 语言也提供了类似的机制 deferrecover

  1. func get(index int) (ret int) {
  2. defer func() {
  3. if r := recover(); r != nil {
  4. fmt.Println("Some error happened!", r)
  5. ret = -1
  6. }
  7. }()
  8. arr := [3]int{2, 3, 4}
  9. return arr[index]
  10. }
  11. func main() {
  12. fmt.Println(get(5))
  13. fmt.Println("finished")
  14. }
  1. $ go run .
  2. Some error happened! runtime error: index out of range [5] with length 3
  3. -1
  4. finished
  • 在 get 函数中,使用 defer 定义了异常处理的函数,在协程退出前,会执行完 defer 挂载的任务。因此如果触发了 panic,控制权就交给了 defer。
  • 在 defer 的处理逻辑中,使用 recover,使程序恢复正常,并且将返回值设置为 -1,在这里也可以不处理返回值,如果不处理返回值,返回值将被置为默认值 0。

6 结构体,方法和接口

6.1 结构体 (struct) 和方法 (methods)

结构体类似于其他语言中的 class,可以在结构体中定义多个字段,为结构体实现方法,实例化等。接下来我们定义一个结构体 Student,并为 Student 添加 name,age 字段,并实现 hello() 方法。

  1. type Student struct {
  2. name string
  3. age int
  4. }
  5. func (stu *Student) hello(person string) string {
  6. return fmt.Sprintf("hello %s, I am %s", person, stu.name)
  7. }
  8. func main() {
  9. stu := &Student{
  10. name: "Tom",
  11. }
  12. msg := stu.hello("Jack")
  13. fmt.Println(msg)
  14. }
  • 使用 Student{field: value, ...}的形式创建 Student 的实例,字段不需要每个都赋值,没有显性赋值的变量将被赋予默认值,例如 age 将被赋予默认值 0。
  • 实现方法与实现函数的区别在于,func 和函数名hello 之间,加上该方法对应的实例名 stu 及其类型 *Student,可以通过实例名访问该实例的字段name和其他方法了。
  • 调用方法通过 实例名. 方法名 (参数) 的方式。

除此之外,还可以使用 new 实例化:

  1. func main() {
  2. stu2 := new(Student)
  3. fmt.Println(stu2.hello("Alice"))
  4. }

6.2 接口 (interfaces)

一般而言,接口定义了一组方法的集合,接口不能被实例化,一个类型可以实现多个接口。

举一个简单的例子,定义一个接口 Person和对应的方法 getName()getAge()

  1. type Person interface {
  2. getName() string
  3. }
  4. type Student struct {
  5. name string
  6. age int
  7. }
  8. func (stu *Student) getName() string {
  9. return stu.name
  10. }
  11. type Worker struct {
  12. name string
  13. gender string
  14. }
  15. func (w *Worker) getName() string {
  16. return w.name
  17. }
  18. func main() {
  19. var p Person = &Student{
  20. name: "Tom",
  21. age: 18,
  22. }
  23. fmt.Println(p.getName())
  24. }
  • Go 语言中,并不需要显式地声明实现了哪一个接口,只需要直接实现该接口对应的方法即可。
  • 实例化 Student后,强制类型转换为接口类型 Person。

在上面的例子中,我们在 main 函数中尝试将 Student 实例类型转换为 Person,如果 Student 没有完全实现 Person 的方法,比如我们将 (*Student).getName() 删掉,编译时会出现如下报错信息。

  1. *Student does not implement Person (missing getName method)

但是删除 (*Worker).getName() 程序并不会报错,因为我们并没有在 main 函数中使用。这种情况下我们如何确保某个类型实现了某个接口的所有方法呢?一般可以使用下面的方法进行检测,如果实现不完整,编译期将会报错。

  1. var _ Person = (*Student)(nil)
  2. var _ Person = (*Worker)(nil)
  • 将空值 nil 转换为 *Student 类型,再转换为 Person 接口,如果转换失败,说明 Student 并没有实现 Person 接口的所有方法。
  • Worker 同上。

实例可以强制类型转换为接口,接口也可以强制类型转换为实例。

  1. func main() {
  2. var p Person = &Student{
  3. name: "Tom",
  4. age: 18,
  5. }
  6. stu := p.(*Student)
  7. fmt.Println(stu.getAge())
  8. }

6.3 空接口

如果定义了一个没有任何方法的空接口,那么这个接口可以表示任意类型。例如

  1. func main() {
  2. m := make(map[string]interface{})
  3. m["name"] = "Tom"
  4. m["age"] = 18
  5. m["scores"] = [3]int{98, 99, 85}
  6. fmt.Println(m)
  7. }

7 并发编程 (goroutine)

7.1 sync

Go 语言提供了 sync 和 channel 两种方式支持协程 (goroutine) 的并发。

例如我们希望并发下载 N 个资源,多个并发协程之间不需要通信,那么就可以使用 sync.WaitGroup,等待所有并发协程执行结束。

  1. import (
  2. "fmt"
  3. "sync"
  4. "time"
  5. )
  6. var wg sync.WaitGroup
  7. func download(url string) {
  8. fmt.Println("start to download", url)
  9. time.Sleep(time.Second)
  10. wg.Done()
  11. }
  12. func main() {
  13. for i := 0; i < 3; i++ {
  14. wg.Add(1)
  15. go download("a.com/" + string(i+'0'))
  16. }
  17. wg.Wait()
  18. fmt.Println("Done!")
  19. }
  • wg.Add(1):为 wg 添加一个计数,wg.Done(),减去一个计数。
  • go download():启动新的协程并发执行 download 函数。
  • wg.Wait():等待所有的协程执行结束。
  1. $ time go run .
  2. start to download a.com/2
  3. start to download a.com/0
  4. start to download a.com/1
  5. Done!
  6. real 0m1.563s

可以看到串行需要 3s 的下载操作,并发后,只需要 1s。

7.2 channel

  1. var ch = make(chan string, 10)
  2. func download(url string) {
  3. fmt.Println("start to download", url)
  4. time.Sleep(time.Second)
  5. ch <- url
  6. }
  7. func main() {
  8. for i := 0; i < 3; i++ {
  9. go download("a.com/" + string(i+'0'))
  10. }
  11. for i := 0; i < 3; i++ {
  12. msg := <-ch
  13. fmt.Println("finish", msg)
  14. }
  15. fmt.Println("Done!")
  16. }

使用 channel 信道,可以在协程之间传递消息。阻塞等待并发协程返回消息。

  1. $ time go run .
  2. start to download a.com/2
  3. start to download a.com/0
  4. start to download a.com/1
  5. finish a.com/2
  6. finish a.com/1
  7. finish a.com/0
  8. Done!
  9. real 0m1.528s

8 单元测试 (unit test)

假设我们希望测试 package main 下 calc.go 中的函数,要只需要新建 calc_test.go 文件,在calc_test.go中新建测试用例即可。

  1. package main
  2. func add(num1 int, num2 int) int {
  3. return num1 + num2
  4. }
  1. package main
  2. import "testing"
  3. func TestAdd(t *testing.T) {
  4. if ans := add(1, 2); ans != 3 {
  5. t.Error("add(1, 2) should be equal to 3")
  6. }
  7. }

运行 go test,将自动运行当前 package 下的所有测试用例,如果需要查看详细的信息,可以添加-v参数。

  1. $ go test -v
  2. === RUN TestAdd
  3. --- PASS: TestAdd (0.00s)
  4. PASS
  5. ok example 0.040s

9 包 (Package) 和模块(Modules)

9.1 Package

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

比如我们新建一个文件 calc.gomain.go 平级,分别定义 add 和 main 方法。

  1. package main
  2. func add(num1 int, num2 int) int {
  3. return num1 + num2
  4. }
  1. package main
  2. import "fmt"
  3. func main() {
  4. fmt.Println(add(3, 5))
  5. }

运行 go run main.go,会报错,add 未定义:

  1. ./main.go:6:14: undefined: add

因为 go run main.go 仅编译 main.go 一个文件,所以命令需要换成

  1. $ go run main.go calc.go
  2. 8

  1. $ go run .
  2. 8

Go 语言也有 Public 和 Private 的概念,粒度是包。如果类型 / 接口 / 方法 / 函数 / 字段的首字母大写,则是 Public 的,对其他 package 可见,如果首字母小写,则是 Private 的,对其他 package 不可见。

9.2 Modules

Go Modules 是 Go 1.11 版本之后引入的,Go 1.11 之前使用 $GOPATH 机制。Go Modules 可以算作是较为完善的包管理工具。同时支持代理,国内也能享受高速的第三方包镜像服务。接下来简单介绍 go mod 的使用。Go Modules 在 1.13 版本仍是可选使用的,环境变量 GO111MODULE 的值默认为 AUTO,强制使用 Go Modules 进行依赖管理,可以将 GO111MODULE 设置为 ON。

在一个空文件夹下,初始化一个 Module

  1. $ go mod init example
  2. go: creating new go.mod: module example

此时,在当前文件夹下生成了go.mod,这个文件记录当前模块的模块名以及所有依赖包的版本。

接着,我们在当前目录下新建文件 main.go,添加如下代码:

  1. package main
  2. import (
  3. "fmt"
  4. "rsc.io/quote"
  5. )
  6. func main() {
  7. fmt.Println(quote.Hello())
  8. }

运行 go run .,将会自动触发第三方包 rsc.io/quote的下载,具体的版本信息也记录在了go.mod中:

  1. module example
  2. go 1.13
  3. require rsc.io/quote v3.1.0+incompatible

我们在当前目录,添加一个子 package calc,代码目录如下:

  1. demo/
  2. |--calc/
  3. |--calc.go
  4. |--main.go

calc.go 中写入

  1. package calc
  2. func Add(num1 int, num2 int) int {
  3. return num1 + num2
  4. }

在 package main 中如何使用 package cal 中的 Add 函数呢?import 模块名 / 子目录名 即可,修改后的 main 函数如下:

  1. package main
  2. import (
  3. "fmt"
  4. "example/calc"
  5. "rsc.io/quote"
  6. )
  7. func main() {
  8. fmt.Println(quote.Hello())
  9. fmt.Println(calc.Add(10, 3))
  10. }
  1. $ go run .
  2. Ahoy, world!
  3. 13

附 参考



上一篇 « 机器学习笔试面试题 11-20 下一篇 » Go Gin 简明教程
https://geektutu.com/post/quick-golang.html