Go 语言入门指南:进阶语法篇 |豆包MarsCode AI 刷题

62 阅读6分钟

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.gomain.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))  
}