├── Note ├── Hello-Go │ ├── images │ │ └── command-go.png │ ├── GOROOT-GOPATH-GOBIN-and-workspace.md │ ├── How-to-install-Golang.md │ ├── Go-package.md │ └── Go-commands.md ├── Concurrent-programming │ ├── Overviews.md │ ├── Timeout.md │ ├── Buffered-Channels.md │ ├── Range&Close.md │ ├── Channels.md │ ├── Runtime.md │ ├── Select.md │ └── Goroutine.md └── Grammar-rules │ ├── Fundamental-types.md │ ├── Base-element.md │ ├── Advanced-types.md │ └── Flow-control.md └── README.md /Note/Hello-Go/images/command-go.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lita-jerry/Golang-note/HEAD/Note/Hello-Go/images/command-go.png -------------------------------------------------------------------------------- /Note/Concurrent-programming/Overviews.md: -------------------------------------------------------------------------------- 1 | # 并发编程概述 2 | 3 | `Go` 的优势在于拥有基于多线程的并发方式, 在多进程与分布式方面, 也有很好标准库, 在多核时代总能事半功倍 4 | 5 | ## 并发编程基础 6 | 7 | `并发` 是指在同一个时间段内执行多个任务, 以便能够更快速的得到结果 8 | 9 | -------------------------------------------------------------------------------- /Note/Concurrent-programming/Timeout.md: -------------------------------------------------------------------------------- 1 | # 超时 2 | 3 | 有时 `goroutine` 会进入阻塞状态, 为了避免整个程序进入阻塞状态, 可以使用 `select` 来设置超时: 4 | 5 | ``` Go 6 | package main 7 | 8 | import ( 9 | "fmt" 10 | "time" 11 | ) 12 | 13 | func main() { 14 | c := make(chan int) 15 | o := make(chan bool) 16 | go func() { 17 | for { 18 | select { 19 | case v := <- c: 20 | fmt.Println(v) 21 | case <- time.After(5 * time.Second): 22 | fmt.Println("timeout") 23 | o <- true 24 | break 25 | } 26 | } 27 | }() 28 | <- o 29 | } 30 | ``` -------------------------------------------------------------------------------- /Note/Concurrent-programming/Buffered-Channels.md: -------------------------------------------------------------------------------- 1 | # Buffered Channels 2 | 3 | Go允许指定 `channel` 的缓冲大小, 就是指明 `channel` 可以存储多少元素: `ch := make(chan string, 3)`, 创建了存储3个元素的 `string` 型 `channel`, 在这个 `channel` 中, 前3个元素可以无阻塞的写入, 当写入第4个元素时, 代码将会阻塞, 直到其他 `goroutine` 从 `channel` 中读取一些元素, 腾出元素空间 4 | ``` Go 5 | ch := make(chan type, value) 6 | ``` 7 | 8 | 当 `value = 0` 时, `channel` 是无缓冲阻塞读写的,当 `value > 0` 时, `channel` 有缓冲、是非阻塞的, 直到写满 `value` 个元素才阻塞写入 9 | 10 | ``` Go 11 | package main 12 | 13 | import "fmt" 14 | 15 | func main() { 16 | c := make(chan int, 2)//修改2为1就报错,修改2为3可以正常运行 17 | c <- 1 18 | c <- 2 19 | fmt.Println(<-c) 20 | fmt.Println(<-c) 21 | } 22 | //修改为1报如下的错误: 23 | //fatal error: all goroutines are asleep - deadlock! 24 | ``` -------------------------------------------------------------------------------- /Note/Hello-Go/GOROOT-GOPATH-GOBIN-and-workspace.md: -------------------------------------------------------------------------------- 1 | # GOROOT GOPATH GOBIN与workspace 2 | 3 | ## GOROOT 4 | Golang环境的安装目录, Golang安装完成后就已经确定. 5 | 6 | ## GOPATH 7 | 指向workspace的路径, 需要将GOPATH路径添加到环境变量: $GOPATH 8 | > $GOPATH 是Go命令所依赖的重要环境变量. 9 | GOPATH是workspace的根目录, 包含以下文件夹 10 | - src 源文件的位置: .go, .c, .g, .s 11 | - pkg 已编译包(.a)的位置 12 | - bin Go构建可执行文件的位置 13 | go get 工具将包下载到GOPATH的第一目录 14 | > 从Go 1.8开始, 如果GOPATH未设置环境变量, 则默认为$HOME/go(Unix/Linux)和%USERPROFILE%/go(Windows). 15 | > 有些工具假设GOPATH只有一个目录. 16 | 17 | ## GOBIN 18 | 该文件夹用于放置go install 和go get编译main包后生成的二进制文件 19 | 20 | ## workspace 21 | 项目的工作空间, 即存放项目工程的位置, GOPATH是指向workspace根目录的路径. 22 | 23 | ## 面试可能会提问: 24 | Q: 设置GOPATH的意义?
25 | A: 为了集中组织代码和项目工程, 以及代码间相互引用. 26 | 27 | Q: 工作区是指? GOPATH是指?
28 | A: 工作区是指存放工程文件的位置, GOPATH是指向工作区的根目录的路径. 29 | 30 | Q: 编译后的.a文件是什么?
31 | A: .a文件是在Linux下编译的归档文件, 也就是archive文件, 是程序编译后生成的静态库文件. 32 | -------------------------------------------------------------------------------- /Note/Concurrent-programming/Range&Close.md: -------------------------------------------------------------------------------- 1 | # Range和Close 2 | 3 | 我们使用 `Channel` 的 `Buffered` 时, 需要确定读取的次数, 很不方便, 所以也可以通过 `range` 操作 `slice` 或者 `map` 一样操作缓存类型的 `channel` 4 | 5 | ``` Go 6 | package main 7 | 8 | import ( 9 | "fmt" 10 | ) 11 | 12 | func printEvenNumber(n int, c chan int) { 13 | number, i := 0, 0 14 | for ; i < n; number++ { 15 | if (number % 2 == 0) { 16 | c <- number 17 | i++ 18 | } 19 | } 20 | close(c) 21 | } 22 | 23 | func main() { 24 | c := make(chan int, 10) 25 | go printEvenNumber(100, c) 26 | for i := range c { 27 | fmt.Println(i) 28 | } 29 | } 30 | ``` 31 | `for i := range c` 能够不断地读取 `channel` 传递的数据, 直到该 `channel` 被显式的关闭. 在消费方可以通过语法 `v, ok := <- ch` 中的 `ok` 查询 `channel` 是否被关闭, 如果 `ok` 为 `false`, 则表示 `channel` 已经没有任何数据并已经被关闭 32 | > 应确保在生产者中关闭 `channel` 而不是在消费方, 否则容易引起 `pannic` 33 | > `channel` 不像文件操作需要显式关闭, 当你确认没有任何发送的数据了, 或者想显式结束 `range` 循环, 或者想显式的结束 `channel`, 则可以调用 `close` 关闭 -------------------------------------------------------------------------------- /Note/Concurrent-programming/Channels.md: -------------------------------------------------------------------------------- 1 | # Channels 2 | 3 | Go提供了一个很好的通信机制 `channel`, 可以与Unix shell中的双向管道做类比: 可以通过它发送或接收值 4 | 5 | 值只能是特定的类型: `channel` 类型, 定义一个 `channel` 时, 也需要定义发送到 `channel` 的值的类型, 使用 `make` 创建channel: 6 | ``` Go 7 | ci := make(chan int) 8 | cs := make(chan string) 9 | cf := make(chan interface{}) 10 | ``` 11 | 12 | `channel` 通过操作符 `<-` 才发送和接收数据 13 | ``` Go 14 | cs <- v //将v发送到channel cs 15 | v <- cs //接收channel cs的数据 16 | ``` 17 | 18 | 展示一个实际例子: 19 | ``` Go 20 | package main 21 | 22 | import "fmt" 23 | 24 | func sum(a []int, c chan int) { 25 | total := 0 26 | for _, v := range a { 27 | total += v 28 | } 29 | c <- total // send total to c 30 | } 31 | 32 | func main() { 33 | a := []int{7, 2, 8, -9, 4, 0} 34 | 35 | c := make(chan int) 36 | go sum(a[:len(a)/2], c) 37 | go sum(a[len(a)/2:], c) 38 | x, y := <-c, <-c // receive from c 39 | 40 | fmt.Println(x, y, x + y) 41 | } 42 | ``` 43 | > 默认情况下, `channel` 的的发送和接收数据都是阻塞的, 除非另一端已经准备好, 这样就使得 `goroutines` 同步变的更加的简单, 而不需要显式的 `lock` 44 | -------------------------------------------------------------------------------- /Note/Concurrent-programming/Runtime.md: -------------------------------------------------------------------------------- 1 | # Runtime Goroutine 2 | 3 | 在执行一些高并发的计算任务时, 为了尽量利用高性能服务器多核心的特性, 将计算任务并行化, 从而达到降低总计算时间的目的 4 | 5 | ## Gosched 6 | 7 | 出让时间片函数, 在设计并发任务时, 用户可以在每个 `goroutine` 中控制何时主动让出时间片给其他 `goroutine`, 这可以使用 `Gosched()` 函数实现: 8 | ``` Go 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | "runtime" 14 | ) 15 | 16 | func say(s string) { 17 | for i := 0; i < 5; i++ { 18 | runtime.Gosched() 19 | fmt.Println(s) 20 | } 21 | } 22 | 23 | func main() { 24 | go say("world") //开一个新的Goroutines执行 25 | say("hello") //当前Goroutines执行 26 | fmt.Scanln() //防止未执行完毕就退出程序 27 | fmt.Println("quit") 28 | } 29 | 30 | //以上输出: 31 | // hello 32 | // world 33 | // hello 34 | // world 35 | // hello 36 | // world 37 | // hello 38 | // world 39 | // hello 40 | // world 41 | 42 | // quit 43 | ``` 44 | 理论上讲应该如上所示的输出, 但是也会根据运行环境的不同, 输出 `hello` 与 `world` 的顺序可能也会不同 45 | 46 | ## NumCPU 47 | 48 | 有时为了将多个并发执行的 `goroutine` 分配给不同的CPU核心去完成, 用户就需要知道CPU核心的具体数目. 为此, `runtime` 包提供了 `NumCPU()` 函数可以完成这个任务 49 | 50 | 为了观察系统任务调度情况, 还可以使用 `NumGoroutine()` 函数返回正在执行和排队的任务总数 51 | 52 | ## Goexit 53 | 54 | 终止当前 `goroutine`, 如果要强行终止一个 `goroutine` 的执行, 可以调用 `Goexit()` 函数来完成, `Goexit()` 将终止整个堆栈链, 并在内层退出, 但是 `defer` 语句仍然或被执行 55 | -------------------------------------------------------------------------------- /Note/Concurrent-programming/Select.md: -------------------------------------------------------------------------------- 1 | # Select 2 | 3 | 如果存在多个 `channel`, Go提供了一个关键字 `select` 用于监听 `channel` 上的数据流动 4 | 5 | `select` 默认是阻塞的, 只有当监听的 `channel` 中有发送或接收的时候才会运行 6 | 7 | > 当多个 `channel` 都准备好的时候, `select` 是随机选择一个执行的 8 | 9 | ``` Go 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | ) 15 | 16 | func printEvenNumber(c chan int, quit chan int) { 17 | for i := 0;; i++ { 18 | if (i % 2 == 0) { 19 | select { 20 | case c <- i: 21 | case <- quit: 22 | return 23 | default: 24 | fmt.Println("当c阻塞的时候执行这里") 25 | } 26 | } 27 | } 28 | } 29 | 30 | func main() { 31 | c := make(chan int, 10) 32 | quit := make(chan int, 1) 33 | 34 | go func() { 35 | for i := 0; i < 5; i++ { 36 | fmt.Println(<- c) 37 | } 38 | quit <- 0 39 | }() 40 | 41 | printEvenNumber(c, quit) 42 | } 43 | 44 | //程序输出: 45 | // 当c阻塞的时候执行这里 46 | // 当c阻塞的时候执行这里 47 | // 当c阻塞的时候执行这里 48 | // 当c阻塞的时候执行这里 49 | // 当c阻塞的时候执行这里 50 | // 当c阻塞的时候执行这里 51 | // 当c阻塞的时候执行这里 52 | // 当c阻塞的时候执行这里 53 | // 当c阻塞的时候执行这里 54 | // 当c阻塞的时候执行这里 55 | // 当c阻塞的时候执行这里 56 | // 当c阻塞的时候执行这里 57 | // 0 58 | // 2 59 | // 4 60 | // 6 61 | // 8 62 | ``` 63 | 64 | `select` 中还有 `default` 语法, 当所有的 `channel` 都没有准备好时, 默认执行 `default` 中的语句, 此时不在阻塞等待 65 | -------------------------------------------------------------------------------- /Note/Concurrent-programming/Goroutine.md: -------------------------------------------------------------------------------- 1 | # Goroutine 2 | 3 | `Goroutine` 其实就是协成, 但是比线程更小, 十几个 `goroutine ` 在底层体现就是五六个线程, `Go` 语言内部实现了这些 `Goroutine` 之间的内存共享 4 | 5 | 执行 `goroutine` 只需要极少的栈内存(大概4~5KB), 当然会根据相应的数据伸缩, 也正是因此, 可同时运行成千上万个并发任务, `goroutine ` 比 `thread` 更易用、更高效、更轻便 6 | 7 | `goroutine` 是通过 `Go` 的 `runtime` 管理的一个线程管理器, `goroutine` 通过 `go` 关键字实现, 其实就是一个普通的函数: 8 | ``` Go 9 | go hello(name) 10 | ``` 11 | 12 | 来看一个具体的例子: 13 | ``` Go 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "runtime" 19 | ) 20 | 21 | func say(s string) { 22 | for i := 0; i < 5; i++ { 23 | runtime.Gosched() 24 | fmt.Println(s) 25 | } 26 | } 27 | 28 | func main() { 29 | go say("world") //开一个新的Goroutines执行 30 | say("hello") //当前Goroutines执行 31 | fmt.Scanln() //防止未执行完毕就退出程序 32 | fmt.Println("quit") 33 | } 34 | 35 | //以上输出: hello与world的打印顺序可能因为运行环境不同而不同 36 | // hello 37 | // world 38 | // hello 39 | // world 40 | // hello 41 | // world 42 | // hello 43 | // world 44 | // hello 45 | // world 46 | 47 | // quit 48 | ``` 49 | > runtime.Gosched()表示出让时间片, 让出当前 `goroutine` 执行权限, 调度器会安排其他等待的任务去运行, 并在下轮某个时间片再从该位置恢复执行 50 | 多个 `goroutine` 运行在同一个进程里面, 共享内存数据, 不过设计上我们要遵循 `不要通过共享来通信, 而是要通过通信来共享` 51 | > 默认情况下, 在 `Go 1.5` 将标识并发系统个数的`runtime.GOMAXPROCS`的初始值由 `1` 改为了运行环境的CPU核心数, 如果在 `Go 1.5` 及其以前的环境使用线程, 需要显示的配置核心数量: `runtime.GOMAXPROCS(n)` 52 | -------------------------------------------------------------------------------- /Note/Hello-Go/How-to-install-Golang.md: -------------------------------------------------------------------------------- 1 | # 如何安装Golang环境 2 | 3 | ## 下载安装包 4 | 5 | [官网下载](https://golang.google.cn/dl/) 6 | 7 | ## 傻瓜式安装 8 | 9 | macOS为例, 安装至 /usr/local/go 10 | 11 | ## 配置环境变量 12 | 13 | > 可以写个配置脚本 14 | 15 | ``` sh 16 | vim ~/.bash_profile 17 | ``` 18 | 19 | > 写入环境变量 20 | 21 | ``` 22 | export GOPATH=/Users/YouName/GolangWorkSpace/go 23 | export GOBIN=/Users/YouName/GolangWorkSpace/go/bin 24 | export PATH=$PATH:$GOBIN 25 | ``` 26 | 27 | > 执行脚本 28 | 29 | ``` sh 30 | source ~/.bash_profile 31 | ``` 32 | 33 | > 查看当前环境变量 34 | 35 | ``` sh 36 | go env 37 | ``` 38 | 39 | > 指令出现当前Golang的环境变量, 主要查看GOROOT、GOPATH、GOBIN 40 | 41 | ``` 42 | GOROOT="/usr/local/go" 43 | GOPATH="/Users/YouName/GolangWorkSpace/go" 44 | GOBIN="/Users/YouName/GolangWorkSpace/go/bin" 45 | ``` 46 | 47 | ## 编写第一个Go程序 48 | 49 | > 创建工程 50 | 51 | ``` sh 52 | cd $GOPATH 53 | mkdir src bin pkg 54 | cd src/ 55 | mkdir hello 56 | cd hello/ 57 | vim main.go 58 | ``` 59 | 60 | > 在main.go中编写helloworld代码 61 | 62 | ``` 63 | package main 64 | 65 | import ( 66 | "fmt" 67 | ) 68 | 69 | func main() { 70 | fmt.Println("hello world!") 71 | } 72 | ``` 73 | 74 | > 编译安装 75 | 76 | ``` sh 77 | go install 78 | ``` 79 | 80 | > 生成的可执行文件hello, 存放在$GOPATH/bin下, 可运行查看结果 81 | 82 | ``` sh 83 | $GOPATH/bin/hello 84 | ``` 85 | 86 | ## 关于IDE和插件 87 | 88 | 支持jetbrains全家桶(按年付费, 刚买的...肉疼): [GoLand](https://www.jetbrains.com/go/) 89 | 90 | LiteIDE是一款专门为Go语言开发的跨平台轻量级集成开发环境(IDE), 由visualfc编写: [GitHub](https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/01.4.md) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang学习笔记 2 | 3 | ## Hello Go 4 | 5 | ### [如何安装Golang环境](https://github.com/JerrySir/Golang-note/blob/master/Note/Hello-Go/How-to-install-Golang.md) 6 | 7 | ### [GOROOT GOPATH GOBIN与workspace](https://github.com/JerrySir/Golang-note/blob/master/Note/Hello-Go/GOROOT-GOPATH-GOBIN-and-workspace.md) 8 | 9 | ### [Go命令](https://github.com/JerrySir/Golang-note/blob/master/Note/Hello-Go/Go-commands.md) 10 | 11 | ### [Go包](https://github.com/JerrySir/Golang-note/blob/master/Note/Hello-Go/Go-package.md) 12 | 13 | 14 | ## 语法 15 | 16 | ### [基本元素](https://github.com/JerrySir/Golang-note/blob/master/Note/Grammar-rules/Base-element.md) 17 | 18 | ### [基本类型](https://github.com/JerrySir/Golang-note/blob/master/Note/Grammar-rules/Fundamental-types.md) 19 | 20 | ### [高级类型](https://github.com/JerrySir/Golang-note/blob/master/Note/Grammar-rules/Advanced-types.md) 21 | 22 | ### [流程控制](https://github.com/JerrySir/Golang-note/blob/master/Note/Grammar-rules/Flow-control.md) 23 | 24 | ## 并发编程 25 | 26 | ### [概述](https://github.com/JerrySir/Golang-note/blob/master/Note/Concurrent-programming/Overviews.md) 27 | 28 | ### [Goroutine](https://github.com/JerrySir/Golang-note/blob/master/Note/Concurrent-programming/Goroutine.md) 29 | 30 | ### [Channels](https://github.com/JerrySir/Golang-note/blob/master/Note/Concurrent-programming/Channels.md) 31 | 32 | ### [Buffered Channels](https://github.com/JerrySir/Golang-note/blob/master/Note/Concurrent-programming/Buffered-Channels.md) 33 | 34 | ### [Range和Close](https://github.com/JerrySir/Golang-note/blob/master/Note/Concurrent-programming/Range&Close.md) 35 | 36 | ### [Select](https://github.com/JerrySir/Golang-note/blob/master/Note/Concurrent-programming/Select.md) 37 | 38 | ### [Runtime](https://github.com/JerrySir/Golang-note/blob/master/Note/Concurrent-programming/Runtime.md) -------------------------------------------------------------------------------- /Note/Hello-Go/Go-package.md: -------------------------------------------------------------------------------- 1 | # 包 2 | 3 | ## 基本规则 4 | 5 | - 同目录下的源码文件, 代码包声明语句要一致, 也就是说, 它们要属于同一个代码包, 这对于所有源码文件都是适用的. 6 | - 源码文件中, 声明的包的名字, 可以与其所在目录的名字不同. 7 | 8 | 每个Go程序都是由包构成的, 且都是从main开始加载, 在载入程序之前, 会执行各个程序包内init方法, 在执行main方法时, 已经执行完成所有程序包的init方法 9 | 10 | ``` Go 11 | package main 12 | 13 | import ( 14 | "fmt" 15 | "math/rand" 16 | ) 17 | 18 | func main() { 19 | fmt.Println("The random number is", rand.Intn(10)) 20 | } 21 | ``` 22 | 23 | 本程序通过导入路径 "fmt" 和 "math/rand" 两个包. 24 | 25 | 按照约定, 包名与导入路径最后一个元素一致, 例如"math/rand"包中源码均以`package rand`语句开始. 26 | 27 | > 此程序的运行环境是固定的, 因此`rand.Intn`总是会返回相同的数字(要得到不同的数字, 需为生成器提供不同的种子数, 参见[rand.Seed](https://go-zh.org/pkg/math/rand/#Seed) 练习场中的时间为常量, 因此你需要用其它的值作为种子数) 28 | 29 | 如果需要导入多个包时, 需要用括号括起来, 且每个包名独占一行 30 | 31 | 在使用包中公开的程序实体时, 需要使用包路径最后一个元素加`.`符号访问公开实体, 例如程序中`rand.Intn(10)`方式访问`rand`包中的`Intn`方法 32 | 33 | 同一个源码文件中导入多个包的最后一个元素不能重复(包括只引用不使用的情况), 否则编译错误, 但可以为导入的包起个别名: 34 | 35 | ``` Go 36 | import ( 37 | "xxx1/package" 38 | package2 "xxx2/package" 39 | ) 40 | ``` 41 | 42 | 之后就可以使用`package2.xxx`调用该包的实体 43 | 44 | 如果不想加前缀而直接使用某个包中的实体, 可以使用`.`来代替别名, 例如: 45 | 46 | ``` Go 47 | package main 48 | 49 | import ( 50 | "fmt" 51 | . "math/rand" 52 | ) 53 | 54 | func main() { 55 | fmt.Println("The random number is", Intn(10)) 56 | } 57 | ``` 58 | 59 | 如果只是想初始化某个包, 而不需要在当前源码文件中使用, 可以使用`_`来代替别名, 例如: 60 | 61 | ``` Go 62 | package main 63 | 64 | import ( 65 | "fmt" 66 | _ "math/rand" 67 | ) 68 | 69 | func main() { 70 | fmt.Println("HelloWorld") 71 | } 72 | ``` 73 | 74 | ## 关于实体 75 | 76 | Go中的变量、常量、函数和类型声明可统称为程序实体, 而他们的名称都成为标识符 77 | 78 | 标识符可以使Unicode字符集中任意自然语言文字的字符、数字及下划线`_`, 标识符不能以数字或下划线开头 79 | 80 | ## 关于访问权限 81 | 82 | 标识符的首字母控制着对应程序实体的访问权限, 如果标识符的首字母是大写, 则表示它所对应的程序实体可以被引入该包的程序访问、使用, 也称为可导出或者公开的; 否则, 对应的程序实体就只能被本包内的代码访问, 也称为不可导出的或包级私有的 83 | 84 | ## 包初始化 85 | 86 | ``` Go 87 | func init() { 88 | fmt.Println("Initialize...") 89 | } 90 | ``` 91 | 92 | `init`方法在执行`main`方法之前 -------------------------------------------------------------------------------- /Note/Grammar-rules/Fundamental-types.md: -------------------------------------------------------------------------------- 1 | # Go基本类型 2 | 3 | | 名称 | 宽度(字节) | 零值 | 说明 | 4 | | ------------ | ------------ | ------------ | ------------ | 5 | | bool | 1 | false | 布尔类型 | 6 | | byte | 1 | 0 | 字节类型, 也可看作是一个由8位二进制数表示的无符号整数类型 | 7 | | rune | 4 | 0 | rune类型, 用于存储Unicode字符, 也可看作是一个由32位二进制数表示的有符号整数类型 | 8 | | int/uint | - | 0 | 有符号整数类型/无符号整数类型, 宽度与其平台有关 | 9 | | int8/uint8 | 1 | 0 | 由8位二进制数表示的有符号整数类型/无符号整数类型 | 10 | | int16/uint16 | 2 | 0 | 由16位二进制数表示的有符号整数类型/无符号整数类型 | 11 | | int32/uint32 | 4 | 0 | 由32位二进制数表示的有符号整数类型/无符号整数类型 | 12 | | int64/uint64 | 8 | 0 | 由64位二进制数表示的有符号整数类型/无符号整数类型 | 13 | | float32 | 4 | 0.0 | 由32位二进制数表示的浮点数类型 | 14 | | float64 | 8 | 0.0 | 由64位二进制数表示的浮点数类型 | 15 | | complex64 | 8 | 0.0 + 0.0i | 由64位二进制数表示的复数类型, 它由float32类型的实部和虚部联合表示 | 16 | | complex128 | 16 | 0.0 + 0.0i | 由128位二进制数表示的复数类型, 它由float64类型的实部和虚部联合表示 | 17 | | string | - | "" | 字符串类型, 一旦创建, 其内容不可改变 | 18 | 19 | 基本类型共`18`个, 其中`int`和`uint`的实际宽度会根据计算架构的不同而不同(386: 32比特, 4字节; amd64/x86-64: 64比特, 8字节) 20 | 21 | byte可看作类型`uint8`的别名类型, 而`rune`可看作`int32`的别名类型, rune类型专用于存储Unicode编码的单个字符, 我们可以用5中方法表示一个`rune`字面量: 22 | 23 | - 所对应的字符, 必须是Unicode编码规范所支持的 24 | - 使用 `\x` 为前导并后跟2位十六进制数, 这种方式可以表示宽度为1字节的值, 即一个ASCII编码值 25 | - 使用 `\` 为前导并后跟3位八进制数, 这种表示法也只能表示有限宽度的值, 即它只能用于表示在0和255之间的值, 它与上一个表示法的表示范围是一致的 26 | - 使用 `\u` 为前导并后跟4位十六进制数, 它只能用于表示2字节宽度的值 27 | - 使用 `\U` 为前导并后跟8位十六进制数, 它只能用于表示4字节宽度的值, 这种方式即为Unicode编码规范中的UCS-4表示法 28 | 29 | 此外, rune字面量还支持一类特殊的字符序列————转义符, 转义符的表示方式是在 `\` 后面追加一个特定的单字符: 30 | 31 | | 转义符 | Unicode代码点 | 说明 | 32 | | ------------ | ------------ | ------------ | 33 | | `\a` | U+0007 | 告警铃声或蜂鸣声 | 34 | | `\b` | U+0008 | 退格符 | 35 | | `\f` | U+000C | 换页符 | 36 | | `\n` | U+000A | 换行符 | 37 | | `\r` | U+000D | 回车符 | 38 | | `\t` | U+0009 | 水平制表符 | 39 | | `\v` | U+000b | 垂直制表符 | 40 | | `\\` | U+005c | 反斜杠 | 41 | | `\'` | U+0027 | 单引号, 仅在rune字面量中有效 | 42 | | `\"` | U+0022 | 双引号, 仅在string字面量中有效 | 43 | 44 | 除了上述转义符外, rune字面量中以 `\` 为前导的字符序列都是不合法的 45 | 46 | 字符串字面量有两种表示形式: 47 | - 原生字符串字面量: 由反引号 `\`` 包裹 48 | - 解释型字符串字面量: 由双引号 `"` 包裹 49 | 50 | 只有基本类型及其别名类型才可以作为常量的类型, 常量声明使用关键字 `const`, 一般形式: `const 常量名 常量类型 = 常量值` 51 | 52 | ``` Go 53 | const DEFAULT_IP string = '192.168.0.1' 54 | const DEFAULT_PORT int = 8080 55 | ``` 56 | 57 | 可以简写成: 58 | 59 | ``` Go 60 | const ( 61 | DEFAULT_IP string = '192.168.0.1' 62 | DEFAULT_PORT int = 8080 63 | ) 64 | ``` 65 | -------------------------------------------------------------------------------- /Note/Hello-Go/Go-commands.md: -------------------------------------------------------------------------------- 1 | # Go命令 2 | 3 | ## 用法 4 | 5 | ``` sh 6 | go [arguments] 7 | ``` 8 | 9 | 可通过 `go` 命令查看 10 | 11 | ![](images/command-go.png?raw=true) 12 | 13 | 使用 `go help ` 查看该命令的更多信息. 14 | 15 | 接下来我们将介绍一些常用命令及参数的使用方法. 16 | 17 | ## go build 18 | 19 | 编译包和依赖项 20 | 21 | ``` sh 22 | go build [-o output] [-i] [build flags] [packages] 23 | ``` 24 | 25 | - 可单独对一个go文件执行build命令, 也可对一个目录执行build命令, 如果对一个目录执行, 则会对整个目录的go文件进行编译, 同目录下的源码文件的包声明要一致. 26 | 27 | - 如果是普通包(非main), 执行`go build`命令将不会产生文件, 如需要生成文件, 则需要使用`go install`, 生成的对应文件在`$GOPATH/pkg` 28 | 29 | - 如果是main包, 执行`go build`后将在当前目录产生执行文件, 如果需要在`$GOPATH/bin`下生成执行文件, 则需要执行`go install`或`go build -o /path/xx.a`(Windows环境下为xx.exe) 30 | 31 | - `go build`会忽略目录下以'_'或者'.'开头以及以'_test.go'结尾的go文件. 32 | 33 | - 可以针对不同系统编译不同的文件, `go build`将会选择性的编译以系统名为后缀名的文件, 例如xx_linux.go xx_darwin.go xx_windows.go xx_freebsd.go 文件, 如果在Linux环境下编译, 则只编译xx.linux.go, 其他文件将忽略. 34 | 35 | | 标记名称 | 标记描述 | 36 | | ------------ | ------------ | 37 | | -a | 完全编译, 更新全部至最新包, 如果不但要编译依赖的包, 还要安装他们的执行文件, 可以加入标记`-i` | 38 | | -n | 打印编译命令但不执行 | 39 | | -p n | 以n核CPU并行编译, 默认为本机CPU核数 | 40 | | -race | 开启编译的时自动检测数据竞争的情况, 目前只支持64位的机器(linux/amd64, freebsd/amd64, darwin/amd64 windows/amd64) | 41 | | -msan | 启用[memory sanitizer](https://github.com/google/sanitizers/wiki/MemorySanitizer)的互操作, 只支持linux/amd64, linux/arm64 并且只有 Clang/LLVM 作为主C编译器的平台 | 42 | | -v | 在编译的时候打印包的名字 | 43 | | -work | 打印临时编译目录的名称, 并且在编译结束退出后不删除该目录 | 44 | | -x | 打印编译命令, 可以看到具体执行了哪些操作 | 45 | | -o | 指定输出路径、文件名 | 46 | | -i | 编译并且安装包, 可以理解为`go build` + `go install` | 47 | 48 | ## go install 49 | 50 | 编译并安装包及其依赖项 51 | 52 | ``` sh 53 | go install [-i] [build flags] [packages] 54 | ``` 55 | 56 | 该命令在内部执行分为两个步骤 57 | 58 | - 编译生成可执行文件 59 | - 将生成的文件从临时文件夹移动到`$GOPATH/pkg`或`$GOPATH/bin`中. 60 | 61 | > `go install`命令参数与`go build`的参数基本相同. 62 | 63 | ## go get 64 | 65 | 下载并安装包和依赖项 66 | 67 | ``` sh 68 | go get [-d] [-f] [-t] [-u] [-v] [-fix] [-insecure] [build flags] [packages] 69 | ``` 70 | 71 | `go get`执行两步操作, 第一将资源文件下载下来, 第二步执行`go install` 72 | 73 | 工具会根据域名的不同调用不同的下载工具: 74 | 75 | | 资源域名 | 下载工具 | 76 | | ------------ | ------------ | 77 | | GitHub | Git | 78 | | BitBucket | Git, Mercurial | 79 | | Google Code Project Hosting | Git, Mercurial, Subversion | 80 | | Launchpad | Bazaar | 81 | 82 | | 标记名称 | 标记描述 | 83 | | ------------ | ------------ | 84 | | -d | 表示只下载, 但不安装 | 85 | | -u | 让命令利用网络来更新已有代码包及其依赖包, 默认情况下, 该命令只会从网络上下载本地不存在的代码包, 而不会更新已有的代码包 | 86 | | -f | 仅在使用`-u`标记时才有效, 该标记会让命令程序忽略掉对已下载代码包的导入路径的检查, 如果下载并安装的代码包所属的项目是你从别人那里Fork过来的, 那么这样做就尤为重要了 | 87 | | -fix | 包下载完成后, 先执行修正动作, 而后再进行编译和安装 | 88 | | -t | 同时下载测试所需的包 | 89 | | -v | 显示执行的命令 | 90 | | -insecure | 表示可以使用非安全的scheme(如HTTP)下载包, 如下载包所有的网络协议不支持HTTPS, 则可以添加该标记, 并在安全的网络环境使用 | 91 | 92 | > `go get`命令还支持`go build`的参数, 详细请参考`go help build`. 93 | 94 | ## go doc 与 godoc 95 | 96 | 打印Go语言程序实体的文档, Go1.2版本后将`go doc`移到`godoc`中, 需要安装:`go get golang.org/x/tools/cmd/godoc` 97 | 98 | ``` sh 99 | go doc [-u] [-c] [package|[package.]symbol[.methodOrField]] 100 | ``` 101 | 102 | > 所谓程序实体, 是指变量、常量、函数、结构体以及接口 103 | 104 | 启动本地web版文档程序: 105 | ``` sh 106 | godoc -http=:8888 107 | ``` 108 | 109 | 浏览器中打开`127.0, 0.1:8888` 110 | 111 | | 标记名称 | 标记描述 | 112 | | ------------ | ------------ | 113 | | -c | 区分大小写, 默认情况是对程序实体大小写是不敏感的 | 114 | | -cmd | 同时打印出main包中的可导出的程序实体(名称首字母为大写)的文档, 默认不打印该部分文档 | 115 | | -u | 同时打印出不可导出的程序实体(名称首字母为大写)的文档, 默认不打印该部分文档 | 116 | 117 | ## go env 118 | 119 | 打印Go语言的环境信息 120 | 121 | ``` sh 122 | go env [-json] [var ...] 123 | ``` 124 | 125 | 如果加入`-json`标记, 则按json格式打印Go环境信息. 126 | 127 | 更新信息可输入`go help environment`查看 128 | 129 | ## 其他命令 130 | 131 | | 命令名称 | 命令原型 | 命令描述 | 帮助文档 | 132 | | ------------ | ------------ | ------------ | ------------ | 133 | | `go fix` | `go fix [packages]` | 更新包以使用最新的API | `go help packages` | 134 | | `go run` | `go run [build flags] [-exec xprog] package [arguments...]` | 编译并运行程序 | `go help build` | 135 | | `go version` | `go version` | 查看当前Go版本 | 136 | | `go list` | `go list [-f format] [-json] [-m] [list flags] [build flags] [packages]` | 查看当前已经安装的包列表 | 137 | 138 | 更多关于Go命令请参考[The Go Programming Language](https://golang.org/cmd/go/) 139 | -------------------------------------------------------------------------------- /Note/Grammar-rules/Base-element.md: -------------------------------------------------------------------------------- 1 | # Go基本元素 2 | 3 | ## 标识符 4 | 5 | 同一代码块中不允许出现同名的程序实体 6 | 7 | 大小写用于区分访问权限 8 | 9 | 预定义标识符: 10 | 11 | - 所有基本数据类型的名称 12 | - 接口类型error 13 | - 常量true、false和iota 14 | 15 | 所有内建函数的名称: `append`、`cap`、`close`、`complex`、`copy`、`delete`、`imag`、`len`、`make`、`new`、`panic`、`print`、`println`、`real`和`recover` 16 | 17 | > 空标识符`_`用在变量声明或代码包导入, 若声明一个变量而不使用, 则编译器会报错, 使用空标识符`_`就可以绕过编译器检查, 不会产生任何操作 18 | 19 | ## 关键字 20 | 21 | 关键字是被编程语言保留的字符串序列, 不能把他们用作标识符, 因此也叫保留字 22 | 23 | | 类别 | 关键字 | 24 | | ------------ | ------------ | 25 | | 程序声明 | `import`和`package` | 26 | | 程序实体声明和定义 | `chan`、`const`、`func`、`interface`、`map`、`struct`、`type`和`var` | 27 | | 程序流程控制 | `go`、`select`、`break`、`case`、`continue`、`default`、`defer`、`else`、`fallthrough`、`for`、`goto`、`if`、`range`、`return`和`switch` | 28 | 29 | Go的关键字共有`25`个, 其中与并发编程有关的关键字有`go`、`chan`、`select` 30 | 31 | 常用关键字及其解释: 32 | - var 变量声明 33 | - const 常量声明 34 | - package 包声明 35 | - import 包引入 36 | - func 定义函数和方法 37 | - return 函数返回 38 | - defer 类似析构函数, 在函数结束前执行 39 | - go 以协成的方式运行 40 | - select 选择不同类型的通讯 41 | - interface 定义接口 42 | - struct 定义抽象数据类型 43 | - break、case、continue、for、fallthrough、else、if、switch、goto、default 流程控制 44 | - chan channel通讯 45 | - type 声明自定义类型 46 | - map 声明map类型数据 47 | - range 读取slice、map、channel数据 48 | 49 | 特别说明一下关键字`type` ———— 类型声明, 用于声明一个自定义类型: 50 | 51 | ``` Go 52 | type myString string 53 | ``` 54 | 55 | 这里将`myString`的类型声明为`string`类型的一个别名类型, 反过来说, `string`类型是`myString`类型的潜在类型 56 | 57 | 虽然类型及其潜在类型是不同的两个类型, 但是他们的值可以相互转换, 不产生新值: `string(myString("ABC"))` 58 | 59 | ## 字面量 60 | 61 | 简单来说, 字面量的意思就是值的一种标记法 62 | 63 | 常用的字面量有以下三种: 64 | 65 | - 用于表示基础数据类型值的各种字面量, 例如表示浮点数类型值的`3E-2` 66 | - 用于构造各种自定义的复合数据类型的类型字面量, 例如用于表示一个名称为`People`的结构体类型: 67 | 68 | ``` Go 69 | type People struct { 70 | Name string 71 | Sex int 72 | } 73 | ``` 74 | 75 | - 用于表示符合数据类型的值的复合字面量 76 | 77 | ``` Go 78 | People{Name: "ZhangSan", Sex: 1} 79 | ``` 80 | 81 | 其中People表示这个值的类型, 紧随其后的就是由键值对表示的复合元素列表 82 | 83 | ## 操作符 84 | 85 | 也成运算符, 用于执行特定算术或逻辑操作的符号, 操作的对象称为操作数 86 | 87 | | 符号 | 说明 | 示例 | 88 | | ------------ | ------------ | ------------ | 89 | | `\|\|` | 逻辑或操作, 既是二元操作符, 同时也属于逻辑操作符 | `true \|\| false // 结果是true` | 90 | | `&&` | 逻辑与操作, 既是二元操作符, 同时也属于逻辑操作符 | `true && false // 结果是false` | 91 | | `==` | 相等判断操作, 既是二元操作符, 同时也属于比较操作符 | `"abc" == "abc" // 结果是true` | 92 | | `!=` | 不等判断操作, 既是二元操作符, 同时也属于比较操作符 | `"abc" != "abc" // 结果是false` | 93 | | `>` | 大于判断操作, 既是二元操作符, 同时也属于比较操作符 | `3 > 2 // 结果是true` | 94 | | `>=` | 大于或等于判断操作, 既是二元操作符, 同时也属于比较操作符 | `3 >= 2 // 结果是true` | 95 | | `+` | 求和操作, 既是一元操作符又是二元操作符, 同时也属于算数操作符, 若作为一元操作符, 此操作不会对原值产生影响 | `+1 // 结果是1`
`1 + 2 // 结果是3` | 96 | | `-` | 求差操作, 既是一元操作符又是二元操作符, 同时也属于算数操作符, 若作为一元操作符, 则表示求反操作 | `-1 // 结果是-1(1的相反数)`
`1 - 2 // 结果是-1` | 97 | | `\|` | 按位或操作, 既是二元操作符, 同时也属于算数操作符 | `3 \| 5 // 结果是7` | 98 | | `^` | 按位异或操作, 既是一元操作符又是二元操作符, 同时也属于算数操作符, 若作为一元操作符, 则表示按位补码操作 | `3 ^ 5 // 结果是6`
`^6 结果是-7` | 99 | | `*` | 求乘积操作, 既是一元操作符又是二元操作符, 同时也属于算数操作符和地址操作符, 若作为地址操作符, 则表示取值操作 | `*p // 若p为指向整数类型值为3的指针, 则结果为3`
`3 * 5 // 结果是15` | 100 | | `/` | 求商操作, 既是二元操作符, 同时也属于算数操作符 | `9 / 3 // 结果是3` | 101 | | `%` | 求余数操作, 既是二元操作符, 同时也属于算数操作符 | `6 % 5 // 结果是1` | 102 | | `<<` | 按位左移操作, 既是二元操作符, 同时也属于算数操作符 | `4 << 2 // 结果是16` | 103 | | `>>` | 按位右移操作, 既是二元操作符, 同时也属于算数操作符 | `4 >> 2 // 结果是1` | 104 | | `&` | 按位与操作, 既是一元操作符又是二元操作符, 同时也属于算数操作符和地址操作符, 若作为地址操作符, 则表示取址操作 | `&V // 结果为标识符V值在内存中的地址`
`5 | 6 // 结果是4` | 105 | | `&^` | 按位清除操作, 既是二元操作符, 同时也属于算数操作符 | `5 &^ 11 // 结果是4` | 106 | | `!` | 逻辑非操作, 既是一元操作符, 同时也属于逻辑操作符 | `!false // 结果是true` | 107 | | `<-` | 接收操作, 既是一元操作符, 同时也属于接收操作符 | `<- ch // 若ch表示元素类型为type的通道类型值, 则此表达式就表示从ch中接收一个byte类型值的操作` | 108 | 109 | Go的操作符一共有`21`个, 并分为`5`类: `算数操作符`、 `比较操作符`、 `逻辑操作符`、 `地址操作符`、 `接收操作符` 110 | 111 | 当一个表达式中存在多个操作符时, 将涉及到操作顺序的问题, 一元操作符拥有最高的优先级, 而二元操作符执行优先级如下表所示: 112 | 113 | | 优先级 | 操作符 | 114 | | ------------ | ------------ | 115 | | 5 | `*`、 `/`、 `%`、 `<<`、 `>>`、 `&` 和 `&^` | 116 | | 4 | `+`、 `-`、 `\|` 和 `^` | 117 | | 3 | `==`、 `!=`、 `<`、 `<=`、 `>` 和 `>=` | 118 | | 2 | `&&` | 119 | | 1 | `\|\|` | 120 | 121 | 优先级数字越大, 表示优先级越高 122 | 123 | > 注意: `++` 和 `--` 是语句而不是表达式, 因此他们不存在于任何操作符优先层次内, 例如 `*p++` 等同于 `(*p)++` 124 | 125 | ## 表达式 126 | 127 | 表达式是把操作符和函数作用于操作数的计算方法, 在Go中, 表达式是构成具有词法意义的代码的最基本元素, 以下列出表达式的种类: 128 | 129 | | 种类 | 用途 | 示例 | 130 | | ------------ | ------------ | ------------ | 131 | | 选择表达式 | 选择一个值中的字段或方法 | `context.Speaker // context是变量名` | 132 | | 索引表达式 | 选取数组、切片、字符串或字典值中的某个元素 | `array1[1] // array表示一个数组且长度必须大于1` | 133 | | 切片表达式 | 选取数组、数组指针、切片或字符串值中的某个范围的元素 | `slice1[0:2] // slice表示一个切片值, 其容量必须大于或等于2` | 134 | | 类型断言 | 判断一个接口值的实际类型是否为某个类型, 或一个非接口值的类型是否实现了某个接口类型 | `v1.(I1) // v1表示一个接口值, I1表示一个接口类型` | 135 | | 调用表达式 | 调用一个函数或一个值的方法 | `v1.M1() // v1表示一个值, M1表示与该值关联的一个方法` | 136 | 137 | 关于类型断言, 有两点需要注意: 138 | 139 | - 如果v1是一个非接口值, 那么必须在做类型断言之前把它转换成接口值, 因为Go中的任何类型都是空接口类型的实现类型, 所以一般会这样做: `interface{}(v1)(I1)` 140 | 141 | - 如果类型断言的结果为`false`, 就意味着该类型断言是失败的, 失败的类型断言会引发一个运行时恐慌(或称运行时异常), 解决办法是: 142 | 143 | ``` Go 144 | var i1, ok = interface{}(v1)(I1) 145 | ``` 146 | 147 | 这里声明并赋值了两个变量, 其中`ok`是布尔类型的变量, 它的值体现了类型断言的成败, 如果成功, `i1`就会是经过类型转换后的`I1`类型的值, 否则它将会是`I1`类型的零值(或称默认值), 此时当断言失败时, 运行时恐慌就不会发生 148 | 149 | 关键字`var`用于变量的声明, 在它和等号`=`之间可以有多个由逗号隔开的变量名, 这种在一条语句中同时为多个变量赋值的方法, 叫做`平行赋值`. 另外, 如果在声明变量的同时进行赋值, 那么等号左边的变量类型可以省略 150 | 151 | ``` Go 152 | var i1 I1 153 | var ok bool 154 | i1, ok = interface{}(v1).(I1) 155 | // 以上可以简写为: 156 | i1, ok := interface{}(v1).(I1) 157 | ``` 158 | 159 | 这种简写方式只能出现在函数中, 有了符号`:=`, 关键字`var`也可以省略, 这叫`短变量声明` 160 | -------------------------------------------------------------------------------- /Note/Grammar-rules/Advanced-types.md: -------------------------------------------------------------------------------- 1 | # Go高级类型 2 | 3 | Go的高级数据类型相当于自定义数据类型的模板或制作工具 4 | 5 | ## 数组 6 | 7 | 数组(array)是由若干相同类型的元素组成的序列 8 | 9 | ``` Go 10 | var ipv4 [4]uint8 = [4]uint8{192, 168, 0, 1} 11 | ``` 12 | 13 | 如果在函数体内, 可省略 `var` 关键字, 但赋值号必须由 `=` 变为 `:=` 14 | 15 | 数组长度也是数组类型的一部分, 只要数组长度不同, 即使是两个数组类型的元素类型相同, 他们也是不同的类型 16 | 17 | 与string类型一样, 一旦在声明中确定了数组类型的长度, 就无法改变它了 18 | 19 | 数组类型的零值一定是一个不包含任何元素的空数组, 一个类型的零值即为该类型变量未被显示赋值时的默认值, 以ipv4为例, 其所属类型的零值就是`[4]uint8{}` 20 | 21 | 在上面示例中, 等号右边的字面量表示该类型的一个值, 可以忽略在方括号中表示数组长度的正整数值: 22 | ``` Go 23 | [...]uint8{192, 168, 0, 1} 24 | ``` 25 | 方括号中的特殊标记 `...` 表示需要由Go编译器计算该值的元素数量并以此获得其长度 26 | 27 | `索引表达式` 和 `切片表达式` 都可用于数组操作, 前者获取数组值中的一个元素, 后者获取一个元素类型与之相同的切片值 28 | 29 | 此外 `len` 和 `cap` 也都可以应用于数组值, 并都可以得到其长度 30 | 31 | 当需要详细规划程序所用的内存时, 数组类型非常有用, 使用数组值可以完全避免内存的二次分配操作, 因为它的长度是不可变的 32 | 33 | ## 切片 34 | 35 | 切片(slice)可以看做是一种对数组的包装形式, 它包装的数组称为该切片的底层数组. 反过来讲, 切片是针对其底层数组中某个连续片段的描述 36 | 37 | ``` Go 38 | var ips = []string{"192.168.0.1", "192.168.0.2", "192.168.0.3"} 39 | ``` 40 | 41 | 与数组不同, 切片的类型字面量(如`[]string`)并不携带长度信息. 切片的长度是可变的, 并且不是类型的一部分; 只要元素类型相同, 两个切片的类型就是相同的. 此外, 一个切片类型的零值总是 `nil`, 此零值的长度和容量都为0 42 | 43 | 切片值相当于对某个底层数组的引用, 其内部结构包含了3个元素: 44 | - 指向底层数组中某个元素的指针 45 | - 切片的长度 46 | - 切片的容量 47 | 48 | > 这里所说的容量是指: 从指针指向的那个元素到底层数组的最后一个元素的元素个数 49 | 50 | 切片值的容量意味着, 在不更换底层数组的前提下, 它的长度的最大值. 可通过 `cap` 函数和切片表达式, 在此前提下最大化一个切片值的长度: `ips[:cap(ips)]` 51 | 52 | > 简单来说, 切片的截取表达含义, 例如: `ips[0:2]`, 其含义为从切片下标 `0` 开始, 到下标 `2` 之前的这一段数据, 即: `ips[0]、ips[1]` 53 | 54 | 除 `len` 和 `cap`, 内建函数 `append` 也可应用于切片值: `ips = append(ips, "192.168.0.4")` 55 | [append文档](https://golang.org/pkg/builtin/#append) 56 | 57 | 另一个值得提的内置函数是 `make`, 它用于初始化切片、字典或通道类型的值, 对于切片类型来说, 用 `make` 函数的好处就是可以用很短的代码初始化一个长度很大的值: `ips = make([]string, 100)`, 其中100个元素的值都是string的零值: 空字符串"" 58 | 59 | To learn more about slices, read the [Slices: usage and internals](https://blog.golang.org/go-slices-usage-and-internals) article. 60 | 61 | ## 字典 62 | 63 | Go中字典类型是散列表(hash table)的一个实现, 其官方称为 `map`, 散列表是一个实现了关联数组的数据结构, 关联数组是用于表示键值对的无序集合的一种抽象数据类型. Go中称键值对为 `键-元素对`, 它把字典值中的每个键都看作与其对应的元素的索引, 这样的索引在同一个字典值中是唯一的 64 | 65 | ``` Go 66 | var ipSwitches = map[string]bool{} 67 | ``` 68 | 69 | 变量 `ipSwitches` 的键类型为 string, 元素类型为 bool, `map[string]bool{}` 表示了一个不包含任何元素的字典值 70 | 71 | 与切片类型一样, 字典类型也是一个引用类型, 字典类型的零值是 `nil`, 字典值的长度表示了其中的建-元素对的数量, 其零值的长度总是0 72 | 73 | 索引表达式可用于字典值中键-元素的添加和修改: 74 | ``` Go 75 | ipSwitches["192.168.0.1"] = true // 不存在该键, 添加元素值 76 | ipSwitches["192.168.0.1"] = false // 已存在该键, 修改元素值 77 | delete(ipSwitches, "192.168.0.1") // 删除元素, 无论是否存在键, 都会执行完毕 78 | ``` 79 | 80 | ## 函数和方法 81 | 82 | 在Go中, 函数类型是一等类型, 意味着可以把函数当作一个值来传递和使用, 函数值既可以作为其他函数的参数, 也可以作为其结果, 还可以根据函数类型这一特性生成闭包 83 | 84 | 一个函数的声明通常包括关键字 `func`、函数名、分别由圆括号包裹的参数列表和结果列表, 以及由花括号包裹的函数体: 85 | ``` Go 86 | func doSomething(parameter int)(int, error) { 87 | // do something 88 | } 89 | ``` 90 | 91 | 函数可以没有参数列表, 也可以没有结果列表, 但空参数列表必须保留括号, 而空结果列表则不用: 92 | ``` Go 93 | func doSomething() { 94 | // do something 95 | } 96 | ``` 97 | 98 | 另外, 参数列表中的参数必须有名称, 而结果列表中结果的名称则可有可无, 不过结果列表中的结果要么全都省略名称, 要么就全都有名称, 带有结果名称的 `doSomething` 函数的声明: 99 | ``` Go 100 | func doSomething(parameter int)(result int, err error) { 101 | // 如果函数的结果有名称, 以他们为名的变量就会被隐式声明, 在这里就可以直接使用 102 | if parameter == 0 { 103 | err = errors.New("parameter is 0") 104 | return 105 | } 106 | // 给代表结果的变量赋值, 就相当于设置函数的返回结果 107 | result = parameter * 10 108 | return 109 | } 110 | ``` 111 | > Go编程有一个惯用法, 即把 `error` 类型的结果作为函数结果列表的最后一员 112 | 113 | 可以在使用的时候实现函数执行体, 此时需要将函数提升成为一个类型, 调用的时候将函数当成参数传入(也叫闭包) 114 | 115 | ``` Go 116 | // 定义函数类型 117 | type funcType func (parameter1 int, parameter2 int) (result int, err error) 118 | 119 | func callFuncType (a int, b int, func_type funcType) (result int, err error) { 120 | if func_type == nil { 121 | err = errors.New("func_type is nil") 122 | return 123 | } 124 | 125 | return func_type(a, b) 126 | } 127 | ``` 128 | 129 | 上面函数的使用方法: 130 | ``` Go 131 | var func_type funcType = func (parameter1 int, parameter2 int) (result int, err error) { 132 | // 这里可以定义函数执行体内的逻辑 133 | result = parameter1 + parameter2 134 | return 135 | } 136 | 137 | var result, err = callFuncType(1, 2, func_type) 138 | ``` 139 | 140 | 作为一等类型的函数类型让程序的灵活性大大增加, 接口不再是定义行为的唯一途径 141 | 142 | > 函数类型的零值是 `nil`, 检查函数值是否为 `nil` 是有必要的 143 | 144 | 方法是函数的一种, 它实际上就是某个数据类型关联在一起的函数: 145 | ``` Go 146 | type myInt int 147 | 148 | func (x myInt) add(y int) myInt { 149 | x = x + myInt(y) 150 | return x 151 | } 152 | ``` 153 | 154 | 从声明上看, 方法只是在关键字 `func` 和函数名称之间, 加了一个由圆括号包裹的接收者声明. 接收者声明由两部分组成: 155 | - `x` 指定类型的值在当前方法中的标识符 156 | - `myInt` 表明这个方法与哪个类型关联 157 | 158 | > 参数是以数据值传入到方法中, 修改参数 `x` 并不会影响原值(这里指调用者) 159 | ``` Go 160 | x := myInt(1) 161 | y := x.add(2) 162 | fmt.Println(x, y) // 输出: 1 3 163 | ``` 164 | 165 | 值方法接收者类型是非指针的数据类型, 若将方法改为指针方法, 如下: 166 | ``` Go 167 | func (x *myInt) add(y int) myInt { 168 | *x = *x + myInt(y) 169 | return *x 170 | } 171 | ``` 172 | 此时 `fmt.Println(x, y)` 的输出为 `3 3` 173 | 174 | > 对于某个非指针的数据类型, 与它关联的方法的集合中只包含它的值方法. 而对于它的指针类型, 其方法集合中既包含值方法也包含指针方法. 不过, 在非指针数据类型的值上, 也是能够调用其指针方法的, 因为Go在内部做了自动转换. 例如, 若 `add` 方法是指针方法, 那么表达式 `x.add(2)` 会被自动转换为 `(&x).add(2)` 175 | 176 | ### 参数可变型函数 177 | 178 | 参数可变型函数可以使用任何数量的尾随参数, 例如我们使用过的 `fmt.Println()` 179 | ``` Go 180 | package main 181 | 182 | import "fmt" 183 | 184 | // Here's a function that will take an arbitrary number 185 | // of `int`s as arguments. 186 | func sum(nums ...int) { 187 | fmt.Print(nums, " ") 188 | total := 0 189 | for _, num := range nums { 190 | total += num 191 | } 192 | fmt.Println(total) 193 | } 194 | 195 | func main() { 196 | 197 | // Variadic functions can be called in the usual way 198 | // with individual arguments. 199 | sum(1, 2) 200 | sum(1, 2, 3) 201 | 202 | // If you already have multiple args in a slice, 203 | // apply them to a variadic function using 204 | // `func(slice...)` like this. 205 | nums := []int{1, 2, 3, 4} 206 | sum(nums...) 207 | } 208 | ``` 209 | > 如果切片中已经有多个 `args`, 请使用 `func(slice...)` 这样的方法将他们应用于参数可变型函数 210 | 211 | ## 接口 212 | 213 | Go的接口类型用于定义一组行为, 其中每个行为都由一个方法声明表示. 接口类型中的方法声明只有方法签名而没有方法体, 而方法签名包括且仅包括方法的名称、参数列表和结果列表 214 | ``` Go 215 | type Talk interface { 216 | Hello (userName string) string 217 | Talk (heard string) (saying string, end bool, err error) 218 | } 219 | ``` 220 | type、接口类型名称、interface以及由花括号包裹的方法声明集合, 共同组成了一个接口类型声明 221 | > 其中每个方法声明必须独占一行 222 | 223 | 只要一个数据类型的方法集合中包含 `Talk` 接口声明的所有方法, 那么它就一定是 `Talk` 接口的实现类型, 这种接口实现方式完全是 `非入侵式` 的 224 | ``` Go 225 | type myTalk string 226 | 227 | func (talk *myTalk) Hello (userName string) string { 228 | // 省略部分代码 229 | } 230 | 231 | func (talk myTalk) Talk (heard string) (saying string, end bool, err error) { 232 | // 省略部分代码 233 | } 234 | ``` 235 | > 与 `myTalk` 关联的所有方法均为指针方法, 意味着 `myTalk` 类型并不是 `Talk` 接口的实现类型, `*myTalk` 才是 236 | 237 | 一个接口类型的变量可以被赋予任何实现类型的值, 例如: `var talk Talk = new(myTalk)`, 内建函数 `new` 的功能是创建一个指定类型的值, 并返回该值的指针. 若想确定变量 `talk` 中的值是否属于 `*myTalk` 类型, 则可以用类型断言来判断: `_, ok := talk.(*myTalk)` 238 | 239 | Go的数据类型之间并不存在继承关系, 接口类型之间也是如此, 不过, 一个接口类型的声明中可以嵌入任意其他接口类型. 更通俗地讲, 一组行为中可以包含其他的行为组, 而且数量不限 240 | ``` Go 241 | type ChatBot interface { 242 | Hello2 (userName) string 243 | Talk 244 | } 245 | 246 | // 使用ChatBot 247 | var chat ChatBot = new(myTalk) 248 | chat.Hello2("lisi") 249 | chat.Hello("zhangsan") 250 | ``` 251 | 252 | ## 结构体 253 | 254 | 结构体类型不仅可以关联方法, 而且可以有内置元素(又称字段), 结构体类型的声明一般以关键字 `type` 开始, 并依次包含类型名称、关键字 `struct` 以及由花括号包裹的字段声明列表 255 | ``` Go 256 | type simpleCN struct { 257 | name string 258 | talk Talk 259 | } 260 | ``` 261 | 结构体类型中的每个字段声明都需独占一行, 一般情况下, 字段声明需由字段名称和字段类型的字面量组成 262 | 263 | 结构体类型的值一般由复合字面量来表达, 复合字面量可以由类型字面量和由花括号包裹的键值对列表组成, 在同一个结构体字面量中, 一个字段名称只能出现一次 264 | 265 | 可以忽略字段的名称, 但是有两个限制: 266 | - 要么忽略所有字段名称, 要么都不忽略 267 | - 多个字段值的顺序应该与结构体类型中字段声明的顺序一致, 并且不能够省略对任何一字段的赋值. 例如 `simpleCN{"simple.cn", nil}` 是合法的, 而 `simpleCN{nil, "simple.cn"}` 和 `simpleCN{"simple.cn"}` 就不合法. 在不忽略字段名称的写法中, 未被赋值的字段会自动被其类型的零值填充 268 | 269 | > 与数组类型相同, 结构体类型属于值类型, 因此结构体类型的零值不是 `nil`, 例如 `simpleCN` 的零值就是 `simpleCN{}` 270 | 271 | 结构体的匿名字段: 272 | ``` Go 273 | type User struct { 274 | name string 275 | } 276 | 277 | type Team struct { 278 | User // 匿名字段, 那么默认Team就包含了User的所有字段, 也包含User的所有method方法, 即: 字段/方法继承 279 | name string // 假设有字段名冲突, 访问Team.name也只是Team的name字段, 如需访问User的name需要Team. User.name, 即: 字段/方法覆盖 280 | } 281 | ``` 282 | -------------------------------------------------------------------------------- /Note/Grammar-rules/Flow-control.md: -------------------------------------------------------------------------------- 1 | # Go流程控制 2 | 3 | Go在流程控制方面的特点如下: 4 | - 没有 `do` 和 `while` 循环, 只有一个更广义的 `for` 语句 5 | - `switch` 语句灵活多变, 还可以用于类型判断 6 | - `if` 语句和 `switch` 语句都可以包含一条初始化子语句 7 | - `break` 语句和 `continue` 语句可以后跟一条标签(`label`)语句, 以标识需要终止或继续的代码块 8 | - `defer` 语句可以使用我们更加方便地执行异常捕获和资源回收任务 9 | - `select` 语句也可用于多分支选择, 但只与通道配合使用 10 | - `go` 语句用于异步启用 `goroutine` 并执行指定函数 11 | 12 | ## 代码块和作用域 13 | 14 | 代码块就是一个由花括号包裹的表达式和语句的序列, 代码块中也可以不包含任何内容, 即: 空代码块`{}` 15 | 16 | 除了显式的代码块之外, 还有一些隐式的代码块: 17 | - 所有Go代码形成了一个最大的代码块, 即: 全域代码块 18 | - 每一个代码包中的代码共同组成了一个代码块, 即: 代码包代码块 19 | - 每一个源码文件都是一个代码块, 即: 源码文件代码块 20 | - 每一个 `if`、 `for`、 `switch` 和 `select` 语句都是一个代码块 21 | - 每一个在 `switch` 或 `select` 语句中的 `case` 分支都是一个代码块 22 | 23 | 在Go中, 使用代码块表示词法上的作用域范围, 具体规则如下: 24 | - 一个预定义标识符的作用域是全域代码块 25 | - 表示一个常量、变量、类型或函数(不包括方法), 且声明在函数之外的标识符的作用域是当前的代码包代码块 26 | - 被导入的代码包的名称的作用域是当前的源码文件代码块 27 | - 表示方法接收者、方法参数或方法结果的标识符的作用域是当前的方法代码块 28 | - 对于表示常量、变量、类型或函数的标识符, 如果被声明在函数内部, 那么作用域就是包含其声明的那个最内层的代码块 29 | 30 | 此外, 我们还可以重新声明已经在外层代码块中声明过的标识符, 当在内层代码块中使用这个标识符, 它表示的总是在该代码块中与它绑定在一起的那个程序实体, 可以说, 此时在外层代码块中声明的那个同名标识符被屏蔽了 31 | 32 | ## if 语句 33 | 34 | `if` 语句会根据条件表达式来执行两个分支中的一个, 如果表达式的结果是 `true` 则执行 `if` 分支, 否则 `else` 分支会被执行 35 | ``` Go 36 | var number int 37 | // 没有else分支 38 | if number < 100 { 39 | number++ 40 | } 41 | 42 | // 有else分支 43 | if number < 100 { 44 | number++ 45 | } else { 46 | number-- 47 | } 48 | 49 | // 包含初始化子语句, 用于初始化局部变量 50 | if diff := 100 - number; number < 100 { 51 | number++ 52 | } else { 53 | number-- 54 | } 55 | 56 | // 支持串联 57 | if diff := 100 - number; number < 100 { 58 | number++ 59 | } else if number < 200 { 60 | number-- 61 | } else { 62 | number -= 100 63 | } 64 | ``` 65 | 66 | ## switch 语句 67 | 68 | `switch` 语句也提供了一种多分支执行的方法, 它会用一个表达式或类型说明符与每一个 `case` 进行比较, 并决定执行哪一个分支 69 | 70 | ### 表达式 switch 语句 71 | 72 | 在表达式 `switch` 语句中, `switch` 表达式和所有 `case` 携带的表达式(也称为 `case` 表达式)都会被求值, 并且执行顺序是自左向右, 自上而下, 只有第一个与 `switch` 表达式的求值结果相同的 `case` 表达式的分支会执行, 如果没有找到匹配的 `case` 表达式并且存在 `default case`, 那么 `default case` 的分支会执行 73 | > `default case` 最多只能有一个, 另外 `switch` 表达式可以省略, 这时 `true` 会作为(并不存在的) `switch` 表达式的结果 74 | ``` Go 75 | var content string = "Go" 76 | 77 | switch content { 78 | default: 79 | fmt.Printf("default\n") 80 | case "Python": 81 | fmt.Printf("Python\n") 82 | case "Go": { 83 | fmt.Printf("Go") 84 | fmt.Printf("\n") 85 | } 86 | } 87 | 88 | // switch语句可以包含一条子语句来初始化局部变量 89 | switch lang := strings.TrimSpace(content); lang { 90 | default: 91 | fmt.Printf("default\n") 92 | case "Python": 93 | fmt.Printf("Python\n") 94 | case "Go": 95 | fmt.Printf("Go\n") 96 | } 97 | 98 | // 使用fallthrough强制执行后面的case代码, fallthrough不会判断下一条case的结果(或default)是否为true 99 | switch lang := strings.TrimSpace(content); lang { 100 | case "Python": 101 | fmt.Printf("Python\n") 102 | fallthrough 103 | case "Go": 104 | fmt.Printf("Go\n") 105 | case "Java", "C": 106 | fmt.Printf("Other\n") 107 | default: 108 | fmt.Printf("default\n") 109 | } 110 | ``` 111 | > `break` 可以跳出当前 `switch` 语句 112 | 113 | ### 类型 switch 语句 114 | 115 | 类型 `switch` 语句将对类型进行判断, 而不是值 116 | ``` Go 117 | var v interface{} 118 | // do something 119 | switch v.(type) { 120 | case string: 121 | fmt.Printf("string value = %s\n", v) 122 | case int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64: 123 | fmt.Printf("integer value = %d\n", v) 124 | default: 125 | fmt.Printf("unsupported value, type = %T\n", v) 126 | } 127 | ``` 128 | 类型 `switch` 语句的 `switch` 表达式会包含一个特殊的类型断言, 例如 `v.(type)`, 它虽然特殊, 但是也要遵循类型断言的规则; 其次, 每个 `case` 表达式中包含的都是类型字面量而不是表达式; `fallthrough` 语句不允许出现在类型 `switch` 语句中 129 | 130 | 类型 `switch` 语句的 `switch` 表达式还有一种变形写法, 如下: 131 | ``` Go 132 | switch i := v.(type) { 133 | case string: 134 | fmt.Printf("string value = %s\n", i) 135 | case int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64: 136 | fmt.Printf("integer value = %d\n", i) 137 | default: 138 | fmt.Printf("unsupported value, type = %T\n", i) 139 | } 140 | ``` 141 | 这里的 `i := v.(type)` 使经类型转换后的值得以保存, `i` 的类型一定会是 `v` 的值的实际类型 142 | 143 | ## for 语句 144 | 145 | `for` 语句用于根据给定的条件重复执行一个代码块, 这个条件或由 `for` 子句直接给出, 或从 `range` 子句中获得 146 | 147 | ### for 子句 148 | 149 | 一条 `for` 语句可以携带一条 `for` 子句, `for` 子句可以包含初始化子句、条件子句和后置子句 150 | ``` Go 151 | package main 152 | 153 | import "fmt" 154 | 155 | func main() { 156 | 157 | // The most basic type, with a single condition. 158 | i := 1 159 | for i <= 3 { 160 | fmt.Println(i) 161 | i = i + 1 162 | } 163 | 164 | // A classic initial/condition/after `for` loop. 165 | for j := 7; j <= 9; j++ { 166 | fmt.Println(j) 167 | } 168 | 169 | // `for` without a condition will loop repeatedly 170 | // until you `break` out of the loop or `return` from 171 | // the enclosing function. 172 | for { 173 | fmt.Println("loop") 174 | break 175 | } 176 | 177 | // You can also `continue` to the next iteration of 178 | // the loop. 179 | for n := 0; n <= 5; n++ { 180 | if n%2 == 0 { 181 | continue 182 | } 183 | fmt.Println(n) 184 | } 185 | } 186 | ``` 187 | 在 `for` 子句的初始化子句和后置子句同时被省略, 或者其中的部分代码都省略的情况下, 分隔符 `;` 可以省略: 188 | ``` Go 189 | var m = 1 190 | for m < 50 { 省略初始化子句和后置子句 191 | m *= 3 192 | } 193 | ``` 194 | 195 | ### range 子句 196 | 197 | 一条 `for` 语句可以携带一条 `range` 子句, 这样就可以迭代出一个数组或切片值中的每个元素、一个字符串中的每个字符, 或者一个字典中的每个键值对, 以及持续地接收一个通道类型值中的元素, 随着迭代的进行, 每一次获取的迭代值(索引、元素、字符或键值对)都会赋值给相应的迭代变量: 198 | ``` Go 199 | package main 200 | 201 | import "fmt" 202 | 203 | func main() { 204 | 205 | // Here we use `range` to sum the numbers in a slice. 206 | // Arrays work like this too. 207 | nums := []int{2, 3, 4} 208 | sum := 0 209 | for _, num := range nums { 210 | sum += num 211 | } 212 | fmt.Println("sum:", sum) 213 | 214 | // `range` on arrays and slices provides both the 215 | // index and value for each entry. Above we didn't 216 | // need the index, so we ignored it with the 217 | // blank identifier `_`. Sometimes we actually want 218 | // the indexes though. 219 | for i, num := range nums { 220 | if num == 3 { 221 | fmt.Println("index:", i) 222 | } 223 | } 224 | 225 | // `range` on map iterates over key/value pairs. 226 | kvs := map[string]string{"a": "apple", "b": "banana"} 227 | for k, v := range kvs { 228 | fmt.Printf("%s -> %s\n", k, v) 229 | } 230 | 231 | // `range` can also iterate over just the keys of a map. 232 | for k := range kvs { 233 | fmt.Println("key:", k) 234 | } 235 | 236 | // `range` on strings iterates over Unicode code 237 | // points. The first value is the starting byte index 238 | // of the `rune` and the second the `rune` itself. 239 | for i, c := range "go" { 240 | fmt.Println(i, c) 241 | } 242 | } 243 | ``` 244 | > 在 `range` 关键字右边的是 `range` 表达式, `range` 表达式一般只会在迭代开始前被求值一次 245 | 246 | 使用 `range` 子句, 有以下三点需要注意: 247 | - 若对数组、切片或字符串进行迭代, 且 `:=` 左边只有一个迭代变量时, 一定要小心, 这时只会得到其中元素的索引, 而不是元素本身; 这很可能不是你想要的 248 | - 迭代没有任何元素的数组值、为 `nil` 的切片值, 为 `nil` 的字典或为 `""` 的字符串值, 并不会执行 `for` 语句中的代码, `for` 语句在一开始时就会直接结束执行, 因为这些值的长度都为 `0` 249 | - 迭代为 `nil` 的通道值会让当前流程永远阻塞在 `for` 语句上! 250 | 251 | ## defer 语句 252 | 253 | `defer` 语句用于延迟操作, 用于函数体内, 通常用于函数结束后的一系列操作, 例如清理操作 254 | 255 | 有几点需要注意: 256 | - 如果 `defer` 函数中使用外部变量, 应通过参数传入 257 | - 调用 `defer` 函数时若有参数传入, 那么将在 `defer` 执行时求值 258 | - 多个 `defer` 函数调动时, 会与调用顺序相反, 可以想象为将调用压入同一栈, 调用时从栈顶取出 259 | 260 | ``` Go 261 | func printNumbers() { 262 | for i := 0; i < 5; i++ { 263 | defer func(n int) { 264 | fmt.Printf("%d", n) 265 | }(i * 2) 266 | } 267 | } 268 | // 执行结果: 86420 269 | ``` 270 | 271 | ## panic 和 recover 272 | 273 | 通常情况下, 函数向其调用方法报告的错误都是返回一个 `error` 类型的值, 但是遇到致命的轻局昂将会导致程序无法继续运行, Go推荐通过调用 `panic` 函数来报告错误 274 | 275 | ### panic 276 | 277 | 用于停止当前的控制流程并引发一个运行时恐慌, 可以接受一个任意类型的参数值, 不过这个参数值的类型通常是 `string` 或 `error`, 因为这样更容易描述运行时恐慌的信息: 278 | ``` Go 279 | func main() { 280 | panic("a problem") 281 | } 282 | 283 | // 也可以通过Go的运行时系统来引发 284 | fmt.Println([3]int{1, 2, 3}[3]) // 数组越界, 相当于调用panic并传入一个runtime.Error类型的参数值 285 | ``` 286 | > `runtime.Error` 是一个接口类型, 并且内嵌了Go内置的error接口类型 287 | 288 | ### recover 289 | 290 | 运行时恐慌一旦被引发, 就会向调用方传播直至程序崩溃, `recover` 用于“拦截”运行时恐慌的内建函数, 它可以使当前的程序从恐慌状态中恢复并重新获得流程控制权, `recover` 函数被调用后, 会返回一个 `interface{}`类型的结果, 如果当时的程序正处于运行时恐慌的状态, 那么这个结果就会是非 `nil` 的 291 | > `recover` 函数应该与 `defer` 语句搭配起来使用: 292 | ``` Go 293 | defer func() { 294 | if p := recover(); p != nil { 295 | fmt.Printf("Recovered panic: %s\n", p) 296 | } 297 | }() 298 | ``` 299 | 将此段代码放在函数体的开始处, 这样可以有效防止该函数及其下层调用中的代码引发运行时恐慌, 那么这个结果就会是非 `nil`, 一旦发现其结果非 `nil`, 就应该采取相应的措施 300 | 301 | Go标准库中有一种常见的用法值得参考: 302 | ``` Go 303 | // fmt中的Token函数部分声明 304 | func (s *ss) Token(skipSpace bool, f func(rune) bool) (tok []byte, err error) { 305 | defer func() { 306 | if e := recover(); e != nil { 307 | if se, ok := e.(scanError); ok { 308 | err = se.err 309 | } else { 310 | panic(e) 311 | } 312 | } 313 | }() 314 | // 省略部分代码 315 | } 316 | ``` 317 | 在 `Token` 函数包含的延迟函数中, 当运行时恐慌携带值的类型是 `fmt.scanError` 时, 这个值就会被赋值给代表结果值的变量 `err`, 否则运行时恐慌就会被重新引发. 如果这个重新引发的运行时恐慌传递到了调用栈的最顶层, 那么标准输出上就会打印出类型这样的内容: 318 | ``` 319 | panic: <运行时恐慌被首次引发时携带的值的字符串形式> [recovered] 320 | panic: <运行时恐慌被重新引发时携带的值的字符串形式> 321 | 322 | goroutine 1 [running]: 323 | main.func·001() 324 | <调用栈信息> 325 | 326 | goroutine 2 [runnable]: 327 | exit status 2 328 | ``` 329 | 这里展现的惯用法与两个: 330 | - 可以把运行时恐慌的携带值转换为 `error` 类型值, 并当做常规结果返回给调用方, 这样既阻止了运行时恐慌的扩散, 又传递了引起恐慌的原因 331 | - 检查运行时恐慌携带值的类型, 并根据类型做不同的后续动作, 这样可以精确地控制程序的错误处理行为 --------------------------------------------------------------------------------