5.2 错误处理
如果函数实现过程中,如果出现不能处理的错误,可以返回给调用者处理。比如我们调用标准库函数 os.Open 读取文件,os.Open 有2个返回值,第一个是 *File,第二个是 error,如果调用成功,error 的值是 nil,如果调用失败,例如文件不存在,我们可以通过 error 知道具体的错误信息。
import (
"fmt"
"os"
)
func main() {
_. err := os.Open("filename.txt")
if err != nil {
fmt.Println(err)
}
}
// open filename.txt: no such file or directory
可以通过 errow.New 返回自定义的错误
import (
"errors"
"fmt"
)
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 {
fmt.Println(err)
}
}
// error: name is null
在 Python、Java 等语言中有 try...catch 机制,在 try 中捕获各种类型的异常,在 catch 中定义异常处理的行为。Go 语言也提供了类似的机制 defer 和 recover。
func get(index int) (ret int) {
defer func() {
if r := recover(); r != nil {
fmt.Println("Some error happened!", r)
ret = -1
}
}()
arr := [3]int{2, 3, 4}
return arr[index]
}
func main() {
fmt.Println(get(5))
fmt.Println("finished")
}
- 在 get 函数中,使用 defer 定义了异常处理的函数,在协程退出前,会执行完 defer 挂载的任务。因此如果触发了 panic,控制权就交给了 defer。
- 在 defer 的处理逻辑中,使用 recover,使程序恢复正常,并且将返回值设置为 -1,在这里也可以不处理返回值,如果不处理返回值,返回值将被置为默认值 0。
06 结构体、接口和方法
6.1 结构体和方法
结构体类似于其他语言中的 class,可以在结构体中定义多个字段,为结构体实现方法,实例化等。接下来我们定义一个结构体 Student,并为 Student 添加 name,age 字段,并实现 hello() 方法。
type Student struct {
name string
age int
}
func (stu *Student) hello(person string) string {
return fmt.Sprintf("hello %s, I am %s", person, stu.name)
}
func main() {
stu := &Student{
name: "Tom",
}
msg := stu.hello("Jack")
fmt.Println(msg) // hello Jack, I am Tom
}
- 使用
Student{field: value, ...}的形式创建 Student 的实例,字段不需要每个都赋值,没有显性赋值的变量将被赋予默认值,例如 age 将被赋予默认值 0。 - 实现方法与实现函数的区别在于,
func和函数名hello之间,加上该方法对应的实例名stu及其类型*Student,可以通过实例名访问该实例的字段name和其他方法了。 - 调用方法通过
实例名.方法名(参数)的方式。
除此之外,还可以使用 new 实例化:
func main() {
stu2 := new(Student)
fmt.Println(stu2.hello("Alice"))
// hello Alice, I am , name 被赋予默认值""
}
6.2 接口(interfaces)
一般而言,接口定义了一组方法的集合,接口不能被实例化,一个类型可以实现多个接口。
举一个简单的例子,定义一个接口 Person和对应的方法 getName() 和 getAge():
type Person interface {
getName() string
}
type Student struct {
name string
age int
}
func (stu *Student) getName() string {
return stu.name
}
type Worker struct {
name string
gender string
}
func (w *Worker) getName() string {
return w.name
}
func main() {
var p Person = &Student{
name: "Tom",
age: 18,
}
fmt.Println(p.getName()) // Tom
}
- Go 语言中,并不需要显式地声明实现了哪一个接口,只需要直接实现该接口对应的方法即可。
- 实例化
Student后,强制类型转换为接口类型 Person。
在上面的例子中,我们在 main 函数中尝试将 Student 实例类型转换为 Person,如果 Student 没有完全实现 Person 的方法,比如我们将 (*Student).getName() 删掉,编译时会出现如下报错信息。
*Student does not implement Person (missing getName method)
但是删除 (*Worker).getName() 程序并不会报错,因为我们并没有在 main 函数中使用。这种情况下我们如何确保某个类型实现了某个接口的所有方法呢?一般可以使用下面的方法进行检测,如果实现不完整,编译期将会报错。
var _ Person = (*Student)(nil)
var _ Person = (*Worker)(nil)
- 将空值 nil 转换为 *Student 类型,再转换为 Person 接口,如果转换失败,说明 Student 并没有实现 Person 接口的所有方法。
- Worker 同上。
实例可以强制类型转换为接口,接口也可以强制类型转换为实例。
func main() {
var p Person = &Student {
name: "Tom",
age: 18,
}
stu := p.(*Student)
fmt.Println(stu.getAge())
}
6.3 空接口
如果定义了一个没有任何方法的空接口,那么这个接口可以表示任意类型。例如
func main() {
m := make(map[string]interface{})
m["name"] = "Tom"
m["age"] = 18
m["scores"] = [3]int{98, 99, 85}
fmt.Println(m)
}
07 并发编程
7.1 sync
Go 语言提供了 sync 和 channel 两种方式支持协程(goroutine)的并发。
例如我们希望并发下载 N 个资源,多个并发协程之间不需要通信,那么就可以使用 sync.WaitGroup,等待所有并发协程执行结束。
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'))
}
wg.Wait()
fmt.Println("Done!")
}
- wg.Add(1):为 wg 添加一个计数,wg.Done(),减去一个计数。
- go download():启动新的协程并发执行 download 函数。
- wg.Wait():等待所有的协程执行结束。
$ time go run .
start to download a.com/2
start to download a.com/0
start to download a.com/1
Done! real 0m1.563s
可以看到串行需要 3s 的下载操作,并发后,只需要 1s。
7.2 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!")
}
使用 channel 信道,可以在协程之间传递消息。阻塞等待并发协程返回消息。
$ time go run .
start to download a.com/2
start to download a.com/0
start to download a.com/1
finish a.com/2
finish a.com/1
finish a.com/0
Done!
real 0m1.528s
08 单元测试
如果我们希望测试 package main 下 calc.go 中的函数,只需要新建一个 calc_test.go 文件,在文件中新建测试用例即可。
// calc.go
package main
func add(num1 int, num2 int) int {
return num1 + num2
}
// calc_test.go
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")
}
}
运行 go test,将自动运行当前 package 下的所有测试用例,如果需要查看详细的信息,可以添加 -v 参数。
$ go test -v
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok example 0.040s
09 包
9.1 Package
一般来说,一个文件夹可以作为 package,同一个 package 内部变量、方法、类型等定义可以相互看到。
比如我们新建一个文件 calc.go,main.go 平级,分别定义 add 和 main 方法。
// calc.go
package main
func add(num1 int, num2 int) int {
return num1 + num2
}
// main.go
package main
import "fmt"
func main() {
fmt.Println(add(3, 5))
}
运行 go run main.go,会报错,add 未定义:
./main.go:6:14: undefined: add
因为 go run main.go 只编译了一个 main.go 文件
$ go run main.go calc.go
8
Go 语言也有 Public 和 Private 的概念,粒度是包。如果类型/接口/方法/函数/字段的首字母大写,则是 Public 的,对其他 package 可见,如果首字母小写,则是 Private 的,对其他 package 不可见。
9.2 Modules
Go Modules 可以算作是较为完善的包管理工具。同时支持代理,国内也能享受高速的第三方包镜像服务。接下来简单介绍 go mod 的使用。Go Modules 在 1.13 版本仍是可选使用的,环境变量 GO111MODULE 的值默认为 AUTO,强制使用 Go Modules 进行依赖管理,可以将 GO111MODULE 设置为 ON。
在一个空文件夹下,初始化一个 Module
$ go mod init example
go: creating new go.mod: module example
此时,在当前文件夹下生成了 go.mod,这个文件记录当前模块的模块名以及所有依赖包的版本。
接着,我们在当前目录下新建文件 main.go,添加如下代码:
·package main
import (
"fmt"
"rsc.io/quote"
)
func main() {
fmt.Println(quote.Hello())
}
运行 go run .,将会自动触发第三方包 rsc.io/quote 的下载,具体的版本信息也记录在了 go.mod 中。
在当前目录,添加一个子 package calc,代码目录如下:
demo/
|--calc/
|--calc.go
|--main.go
在 calc.go 中写入
package calc
func Add(num1 int, num2 int) int {
return num1 + num2
}
在 package main 中如何使用 package cal 中的 Add 函数呢?import 模块名/子目录名 即可,修改后的 main 函数如下:
package main
import (
"fmt"
"example/calc"
"rsc.io/quote"
)
func main() {
fmt.Println(quote.Hello())
fmt.Println(calc.Add(10, 3))
}