语言特性
- 1: 函数
- 2: 内建函数
- 3: 接口
- 4: 错误处理
- 4.1: 错误处理概述
- 4.2: [博客] 错误处理与go
- 4.3: [博客] 错误是值
- 4.4: [博客] go1.13中的错误处理
- 5: panic
- 5.1: panic概述
1 - 函数
1.1 - 函数概述
在go的世界中,函数是一等公民,可以给变量赋值,可以作为参数传递,也可以直接赋值。
多值返回
https://golang.org/doc/effective_go.html#multiple-returns
Go的一个不同寻常的特点是,函数和方法可以返回多个值。这种形式可以用来改进C程序中的几个笨拙的习语:带内错误返回,如-1代表EOF和修改按地址传递的参数。
在C语言中,写错误的信号是一个负数,错误代码被秘密存放在一个易失性的位置。在Go中,Write可以返回计数(count)和错误(error)。“是的,你写了一些字节,但不是全部,因为你填满了设备”. 来自包os的文件上的Write方法的签名是。
func (file *File) Write(b []byte) (n int, err error)
和文档中说的一样,当 n != len(b) 时,它返回写入的字节数和一个非nil错误。这是一种常见的风格;更多的例子请参见错误处理一节。
类似的方法避免了传递指针到返回值以模拟引用参数的需要。下面是一个简单的函数,用于从字节片中的某个位置抓取一个数字,返回数字和下一个位置。
func nextInt(b []byte, i int) (int, int) {
for ; i < len(b) && !isDigit(b[i]); i++ {
}
x := 0
for ; i < len(b) && isDigit(b[i]); i++ {
x = x*10 + int(b[i]) - '0'
}
return x, i
}
你可以用它来扫描输入切片b中的数字,像这样。
for i := 0; i < len(b); {
x, i = nextInt(b, i)
fmt.Println(x)
}
命名结果参数
Go函数的返回或结果 “参数 “可以被赋予名称,并作为常规变量使用,就像传入参数一样。当命名时,它们在函数开始时被初始化为其类型的零值;如果函数执行一个没有参数的返回语句,结果参数的当前值被用作返回值。
这些名称并不是强制性的,但它们可以使代码更短、更清晰:它们是文档。如果我们给 nextInt 的结果命名,就会很明显地知道哪个返回的 int 是哪个。
func nextInt(b []byte, pos int) (value, nextPos int) {
因为被命名的结果是初始化的,并且与一个不加修饰的返回相联系,所以它们可以简化以及澄清。下面是一个很好地使用它们的 io.ReadFull 版本。
func ReadFull(r Reader, buf []byte) (n int, err error) {
for len(buf) > 0 && err == nil {
var nr int
nr, err = r.Read(buf)
n += nr
buf = buf[nr:]
}
return
}
defer
详见 defer 语句。
函数的本质
摘录自 函数——go世界中的一等公民 “函数的本质” 一节
在go的世界中,函数是一等公民,可以给变量赋值,可以作为参数传递,也可以直接赋值。
函数在go语言里的本质其实就是指向 __TEXT
段内存地址的一个指。
参考资料
1.2 - init 函数
Effective Go
https://golang.org/doc/effective_go.html#init
最后,每个源文件都可以定义自己的niladic init函数来设置任何需要的状态。(其实每个文件都可以有多个init函数。) 而finally的意思是:init是在包中的所有变量声明都评估了初始化器之后才被调用的,而那些初始化器只有在所有导入的包都有初始化器之后才被评估。(其实每个文件都可以有多个init函数。)而finally的意思是最后:init是在包中所有的变量声明都评估了它们的初始化器之后才被调用的,而这些初始化器只有在所有的导入包都被初始化之后才被评估。
除了不能用声明来表达的初始化之外,init函数的一个常见用途是在真正执行开始之前验证或修复程序状态的正确性。
func init() {
if user == "" {
log.Fatal("$USER not set")
}
if home == "" {
home = "/home/" + user
}
if gopath == "" {
gopath = home + "/go"
}
// gopath may be overridden by --gopath flag on command line.
flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}
参考资料
五分钟理解golang的init函数
https://zhuanlan.zhihu.com/p/34211611
init函数的主要作用:
- 初始化不能采用初始化表达式初始化的变量。
- 程序运行前的注册。
- 实现 sync.Once 功能。
- 其他
init函数的主要特点:
- init函数先于main函数自动执行,不能被其他函数调用;
- init函数没有输入参数、返回值;
- 每个包可以有多个init函数;
- 包的每个源文件也可以有多个init函数,这点比较特殊;
- 同一个包的init执行顺序,golang没有明确定义,编程时要注意程序不要依赖这个执行顺序。
- 不同包的init函数按照包导入的依赖关系决定执行顺序。
golang程序初始化:
golang程序初始化先于main函数执行,由runtime进行初始化,初始化顺序如下:
- 初始化导入的包(包的初始化顺序并不是按导入顺序(“从上到下”)执行的,runtime需要解析包依赖关系,没有依赖的包最先初始化,与变量初始化依赖关系类似
- 初始化包作用域的变量(该作用域的变量的初始化也并非按照“从上到下、从左到右”的顺序,runtime解析变量依赖关系,没有依赖的变量最先初始化);
- 执行包的init函数;
几个值得注意的地方:
- 初始化顺序:变量初始化 -> init() -> main()
- 同一个包不同源文件的init函数执行顺序,golang spec没做说明,以上述程序输出来看,执行顺序是源文件名称的字典序。
- init函数不可以被调用,上面代码会提示:undefined: init
- init函数比较特殊,可以在包里被多次定义。
- init函数的主要用途:初始化不能使用初始化表达式初始化的变量
- golang对没有使用的导入包会编译报错,但是有时我们只想调用该包的init函数,不使用包导出的变量或者方法:
import _ "net/http/pprof"
1.3 - 函数调用
理解Go语言中的函数调用
https://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-function-call/
调用惯例
调用惯例是调用方和被调用方对于参数和返回值传递的约定。
-
c
当我们在 x86_64 的机器上使用 C 语言中调用函数时,参数都是通过寄存器和栈传递的,其中:
- 六个以及六个以下的参数会按照顺序分别使用 edi、esi、edx、ecx、r8d 和 r9d 六个寄存器传递;
- 六个以上的参数会使用栈传递,函数的参数会以从右到左的顺序依次存入栈中;
而函数的返回值是通过 eax 寄存器进行传递的,由于只使用一个寄存器存储返回值,所以 C 语言的函数不能同时返回多个值。
-
Go 语言使用栈传递参数和接收返回值,所以它只需要在栈上多分配一些内存就可以返回多个值。
思考:
C 语言和 Go 语言在设计函数的调用惯例时选择也不同的实现。C 语言同时使用寄存器和栈传递参数,使用 eax 寄存器传递返回值;而 Go 语言使用栈传递参数和返回值。我们可以对比一下这两种设计的优点和缺点:
- C 语言的方式能够极大地减少函数调用的额外开销,但是也增加了实现的复杂度;
- CPU 访问栈的开销比访问寄存器高几十倍3;
- 需要单独处理函数参数过多的情况;
- Go 语言的方式能够降低实现的复杂度并支持多返回值,但是牺牲了函数调用的性能;
- 不需要考虑超过寄存器数量的参数应该如何传递;
- 不需要考虑不同架构上的寄存器差异;
- 函数入参和出参的内存空间需要在栈上进行分配;
Go 语言使用栈作为参数和返回值传递的方法是综合考虑后的设计,选择这种设计意味着编译器会更加简单、更容易维护。
参数传递
除了函数的调用惯例之外,Go 语言在传递参数时是传值还是传引用也是一个有趣的问题,这个问题影响的是当我们在函数中对入参进行修改时会不会影响调用方看到的数据。我们先来介绍一下传值和传引用两者的区别:
- 传值:函数调用时会对参数进行拷贝,被调用方和调用方两者持有不相关的两份数据;
- 传引用:函数调用时会传递参数的指针,被调用方和调用方两者持有相同的数据,任意一方做出的修改都会影响另一方。
不同语言会选择不同的方式传递参数,Go 语言选择了传值的方式,无论是传递基本类型、结构体还是指针,都会对传递的参数进行拷贝。
备注:Go的方式和Java是类似的。Java也是传值,如果是对象也传递对象句柄的值)。
总结:
- Go 语言中对于整型和数组类型的参数都是值传递的
- 如果数组很大,传值方式(拷贝)会对性能造成比较大的影响
- 传递结构体时:会对结构体中的全部内容进行拷贝;
- 传递结构体指针时:会对结构体指针进行拷贝;
Go 语言在传递参数时其实使用的就是传值的方式,接收方收到参数时会对这些参数进行复制;
摘要:函数调用的过程
摘录自 函数——go世界中的一等公民 “函数调用的过程” 一节
在go语言中,每一个goroutine持有一个连续栈,栈基础大小为2kb,当栈大小超过预分配大小后,会触发栈扩容,也就是分配一个大小为当前栈2倍的新栈,并且将原来的栈拷贝到新的栈上。使用连续栈而不是分段栈的目的是,利用局部性优势提升执行速度,原理是CPU读取地址时会将相邻的内存读取到访问速度比内存快的多级cache中,地址连续性越好,L1、L2、L3 cache命中率越高,速度也就越快。
在go中,和其他一些语言有所不同,函数的返回值、参数都是由被caller保存。每次函数调用时,会在caller的栈中压入函数返回值列表、参数列表、函数返回时的PC地址,然后更改bp和pc为新函数,执行新函数,执行完之后将变量存到caller的栈空间中,利用栈空间中保存的返回地址和caller的栈基地址,恢复pc和sp回到caller的执行过程。
对于栈变量的访问是通过bp+offset的方式来访问,而对于在堆上分配的变量来说,就是通过地址来访问。在go中,变量被分配到堆上还是被分配到栈上是由编译器在编译时根据逃逸分析决定的,不可以更改,只能利用规则尽量让变量被分配到栈上,因为局部性优势,栈空间的内存访问速度快于堆空间访问。
1.4 - 方法
一般语言中,比如c/c++/Java/python等,函数和方法是等同的。但是在Go语言中,函数和方法有明确的区分,是两个不同的概念:
-
函数/Function:不属于任何结构体、类型,没有接收者
-
func Add(a, b int) int { return a + b }
-
-
方法/Method:和特定的结构体、类型关联,带有接收者
-
type person struct { name string } func (p person) String() string{ return "the person name is "+p.name }
-
简单总结:
Remember: a method is just a function with a receiver argument.
请记住:方法只是一个带有接收者参数的函数。
摘要:Methods vs Functions in Golang
https://medium.com/@ishagirdhar/methods-vs-functions-in-golang-c60586bfa6b4
方法还是函数?函数还是方法?
对于从Java或者其他面向对象的语言背景过来的人看来,第一直觉是处处都用结构体(struct)和方法(method),因为对象的行为总是由方法定义的。但在Golang中,我们既有函数又有方法,这种做法正确吗?
哪些地方我们需要使用方法,哪些地方我们需要函数?
我们先来看看Golang中什么是函数,什么是方法。
**函数(function)**接受一些参数作为输入,并产生一些输出。对于相同的输入,函数总是会产生相同的输出。这意味着它不依赖于状态。类型是作为函数的参数传递的。
Go中的**方法(Method)**是带有特定 receiver(type) 参数上的函数。它定义了类型的行为,它应该使用类型的状态。
但是,如果我们在结构体里面没有状态,那么我们是不是根本就不要定义方法呢?答案是我们可以定义,但这主要是为了对该特定类型的方法进行逻辑分组。
不定义方法的规则是,如果
- 不需要依赖状态
- 可以在任何实现特定接口的类型上执行这个函数,这意味着不需要限制这个函数属于某个特定的类型。
接收者的按值传递
Go语言里有两种类型的接收者:值接收者和指针接收者。
-
使用值接收者: 在调用的时候,方法使用的其实是值接收者的一个副本,所以对该值的任何操作,不会影响原来的值接收者(或者说类型变量)。
-
使用指针接收者:因为指针接收者传递的是一个指向原值指针的副本(即指针的副本),其指向的还是原来类型的值,所以修改时,同时也会影响原来值接收者(类型变量)的值。
总结:
在调用方法的时候,传递的接收者本质上都是副本,只不过可以是值的副本,也可以是指向这个值的指针的副本。指针具有指向原有值的特性,所以修改了指针指向的值,也就修改了原有的值。
Effective Go
Pointers vs. Values
https://golang.org/doc/effective_go.html#pointers_vs_values
正如我们在ByteSize中看到的那样,可以为任何命名类型(除了指针或接口)定义方法;接收者不一定是结构体。
在上面关于切片的讨论中,我们写了一个Append函数。我们可以把它定义为一个关于切片的方法。要做到这一点,我们首先声明一个可以绑定方法的命名类型,然后使该方法的接收者是该类型的值。
type ByteSlice []byte
func (slice ByteSlice) Append(data []byte) []byte {
// Body exactly the same as the Append function defined above.
}
这仍然需要该方法返回更新后的切片。我们可以通过重新定义该方法来消除这种笨拙,将一个指向ByteSlice的指针作为它的接收器,这样该方法就可以覆盖调用者的切片。
func (p *ByteSlice) Append(data []byte) {
slice := *p
// Body as above, without the return.
*p = slice
}
事实上,我们还可以做得更好。如果我们修改函数,使它看起来像一个标准的写方法,像这样。
func (p *ByteSlice) Write(data []byte) (n int, err error) {
slice := *p
// Again as above.
*p = slice
return len(data), nil
}
那么*ByteSlice类型满足标准接口io.Writer,这很方便。例如,我们可以打印成一个。
var b ByteSlice
fmt.Fprintf(&b, "This hour has %d days\n", 7)
关于接收者的指针与值的规则是:值方法可以在指针和值上被调用,但指针方法只能在指针上被调用。
这个规则的产生是因为指针方法可以修改接收者;在值上调用它们会导致该方法接收到一个值的副本,所以任何修改都会被丢弃。因此,语言不允许这种错误。不过有一个方便的例外。当值是可寻址的时候,语言通过自动插入地址操作符来处理在值上调用指针方法的常见情况。在我们的例子中,变量b是可寻址的,所以我们可以只用b.Write来调用它的Write方法。编译器会帮我们改写成 (&b).Write。
顺便说一下,在字节切片上使用Write的想法是实现bytes.Buffer的核心。
摘要:方法的本质
摘录自 函数——go世界中的一等公民 “方法的本质” 一节
go的方法就是语法糖:实际上Method就是将receiver作为函数的第一个参数输入的语法糖而已,本质上和函数没有区别
参考资料
1.5 - 可变参数
golang语言规范
Passing arguments to ...
parameters
https://golang.org/ref/spec#Passing_arguments_to_..._parameters
如果 f 是变数(variadic),其最终参数p的类型为…T,那么在 f 中,p的类型等同于类型 []T。如果调用f时,p没有实际参数,那么传递给p的值就是nil。否则,传递的值是一个类型为 []T 的新的底层数组的分片,其连续的元素是实际的参数,这些参数都必须是可以分配给T的,因此分片的长度和容量是与p绑定的参数数字,对于每个调用点来说可能会有所不同。
给定函数和调用:
func Greeting(prefix string, who ...string)
Greeting("nobody")
Greeting("hello:", "Joe", "Anna", "Eileen")
Greeting在第一次调用时的值为nil,第二次调用时的值为 []string{“Joe”, “Anna”, “Eileen”} 。
如果最后的参数可以分配给一个分片类型 []T,那么如果参数后面有 …T 参数,它将作为 …T 参数的值不变地传递。在这种情况下,不会创建新的分片。
给定分片s并调用
s := []string{"James", "Jasmine"}
Greeting("goodbye:", s...)
在Greeting,它的值将与s的底层数组相同。
摘要:Go 语言“可变参数函数”终极指南
https://studygolang.com/articles/11965
可变参数函数即其参数数量是可变的 —— 0 个或多个。声明可变参数函数的方式是在其参数类型前带上省略符(三个点)前缀。
可变参数的使用场景:
- 避免创建仅作传入参数用的临时切片
- 当参数数量未知
- 传达你希望增加可读性的意图
可变参数函数会在其内部创建一个”新的切片”。事实上,可变参数是一个简化了切片类型参数传入的语法糖。
当不传入参数的时候,可变参数会成为一个空值切片( nil
):
所有的非空切片都有内建的数组,而 nil 切片则没有。
然而,当你向 nil 切片添加元素时,它会自动内建一个包含该元素的数组。这个切片也就再也不是一个 nil 切片了。
可以通过向一个已有的切片添加可变参数运算符 ”…“ 后缀的方式将其传入可变参数函数。
names := []string{"carl", "sagan"}
toFullname(names...)
这就好比通常的传参方式:
toFullname("carl", "sagan")
**不过,这里还是有一点差异:**函数会在内部直接使用这个传入的切片,并不会创建一个的新的。
可以像下面这样将数组转化成切片后传入可变参数函数:
names := [2]string{"carl", "sagan"}
toFullname(names[:]...)
传入的切片和函数内部使用的切片共享同一个底层数组,因此在函数内部改变这个数组的值同样会影响到传入的切片:
参考资料
- Go 语言“可变参数函数”终极指南:推荐阅读,非常详尽
1.6 - 闭包
闭包相关的经典总结:
-
闭包=函数+引用环境
-
对象是附有行为的数据,而闭包是附有数据的行为
https://gobyexample-cn.github.io/closures
Go 支持匿名函数, 并能用其构造闭包。 匿名函数在你想定义一个不需要命名的内联函数时是很实用的。
intSeq
函数返回一个在其函数体内定义的匿名函数。 返回的函数使用闭包的方式 隐藏 变量 i
。 返回的函数 隐藏 变量 i
以形成闭包。
func intSeq() func() int {
i := 0
return func() int {
i++
return i
}
}
我们调用 intSeq
函数,将返回值(一个函数)赋给 nextInt
。 这个函数的值包含了自己的值 i
,这样在每次调用 nextInt
时,都会更新 i
的值。
func main() {
nextInt := intSeq()
// 通过多次调用 nextInt 来看看闭包的效果:
fmt.Println(nextInt()) // 1
fmt.Println(nextInt()) // 2
fmt.Println(nextInt()) // 3
// 为了确认这个状态对于这个特定的函数是唯一的,我们重新创建并测试一下。
newInt2 := intSeq()
fmt.Println(newInt2()) // 1
}
参考资料
1.7 - 递归函数
什么是递归函数:
Technically, a recursive function is a function that makes a call to itself. To prevent infinite recursion, you need an if-else statement (of some sort) where one branch makes a recursive call, and the other branch does not. The branch without a recursive call is usually the base case (base cases do not make recursive calls to the function).
从技术上讲,递归函数是一个对自己进行调用的函数。为了防止无限递归,你需要一个 if-else 语句(或它的某种形式),其中一个分支进行递归调用,而其他分支不进行递归调用。没有递归调用的分支通常是基础案例(基础案例不对函数进行递归调用)。
参考资料
2 - 内建函数
3 - 接口
3.1 - 接口概述
学习资料
- Dig101-Go 之读懂 interface 的底层设计: 非常深入底层,需要认真学习
- go语言接口的原理:需要认真学习 + 1
- Go Data Structures: Interfaces
- how-to-use-interfaces-in-go
3.2 - 接口定义
go语言实战
接口定义和实现
接口类型是由一组方法定义的集合。
// 定义一个interface和它的方法
type Abser interface {
Abs() float64
}
type Vertex struct {
X, Y float64
}
// 让结构体实现interface要求的方法
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
接口类型的值可以存放实现这些方法的任何值。
var a Abser
v := Vertex{3, 4}
a = &v // *Vertex 实现了 Abser
任意接口类型
在go中,如果要表示类型为任意类型,包括基础类型,可以这样做:
type Value struct {
v interface{}
}
有点类似java中的Object,但是go没有对象继承,也没有Object这种单根继承的root对象,为了表示所有类型,就需要使用interface关键字,而interface在go中是关键字,不是类型,因此要加{}
后缀。这个语法相对java有点特别。
Effective Go
接口命名
https://golang.org/doc/effective_go.html#interface-names
按照惯例,单方法接口的命名是由方法名加上 -er 后缀或类似的修饰来构造一个代理名词:Reader, Writer, Formatter, CloseNotifier等等。
这样的名字有很多,尊重它们和它们所使用的函数名是很有成效的。Read、Write、Close、Flush、String等都有规范的标志和含义。为了避免混淆,不要给你的方法起这些名字,除非它有相同的签名和含义。反过来说,如果你的类型实现了一个与一个著名类型上的方法具有相同含义的方法,就给它相同的名称和签名;调用你的字符串转换方法String而不是ToString。
Generality/通用性
https://golang.org/doc/effective_go.html#generality
如果一个类型只是为了实现一个接口而存在,并且永远不会有超出该接口的导出方法,那么就没有必要导出类型本身。只导出接口,就可以清楚地知道该值除了接口中描述的内容之外没有其他需要注意的行为。这也避免了在一个普通方法的每个实例上重复文档的需要。
在这种情况下,构造函数应该返回一个接口值而不是实现类型。举个例子,在哈希库中,crc32.NewIEEE和adler32.New都返回接口类型hash.Hash32。在Go程序中用CRC-32算法代替Adler-32算法,只需要改变构造函数调用,其余代码不受算法改变的影响。
类似的方法使得各种加密包中的流式密码算法与它们链在一起的块密码算法分离。crypto/cipher包中的Block接口指定了块密码的行为,它提供了单个数据块的加密。那么,通过与bufio包类比,实现这个接口的密码包可以用来构造流密码,用Stream接口表示,而不知道块加密的细节。
加密/密文接口是这样的。
type Block interface {
BlockSize() int
Encrypt(dst, src []byte)
Decrypt(dst, src []byte)
}
type Stream interface {
XORKeyStream(dst, src []byte)
}
这里是计数器模式(CTR)流的定义,它将一个块密码变成了一个流密码;注意,块密码的细节被抽象掉了。
// NewCTR returns a Stream that encrypts/decrypts using the given Block in
// counter mode. The length of iv must be the same as the Block's block size.
func NewCTR(block Block, iv []byte) Stream
NewCTR不仅适用于一种特定的加密算法和数据源,而且适用于任何Block接口和任何Stream的实现。因为它们返回的是接口值,所以用其他加密模式替换CTR加密是一个局部的变化。构造函数调用必须被编辑,但由于周围的代码必须只将结果视为Stream,所以它不会注意到这种差异。
接口与方法
https://golang.org/doc/effective_go.html#interface_methods
由于几乎任何东西都可以附加方法,所以几乎任何东西都可以满足接口。一个说明性的例子是在http包中,它定义了Handler接口。任何实现Handler的对象都可以服务于HTTP请求。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
ResponseWriter 本身就是一个接口,它提供了对返回响应给客户端所需方法的访问。这些方法包括标准的 Write 方法,所以 http.ResponseWriter 可以在任何可以使用 io.Writer 的地方使用。Request是一个包含客户端请求的解析表示的结构。
为了简洁起见,我们忽略 POSTs,并假设 HTTP 请求总是 GETs;这种简化并不影响处理程序的设置方式。下面是一个琐碎但完整的处理程序的实现,用来统计页面被访问的次数。
// Simple counter server.
type Counter struct {
n int
}
func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ctr.n++
fmt.Fprintf(w, "counter = %d\n", ctr.n)
}
(为了配合我们的主题,请注意 Fprintf 如何打印到 http.ResponseWriter。) 作为参考,下面是如何将这样的服务器连接到 URL 树上的一个节点。
import "net/http"
...
ctr := new(Counter)
http.Handle("/counter", ctr)
但为什么要把Counter做成一个结构体呢?一个整数就可以了。接收器需要是一个指针,所以增量对调用者来说是可见的)。
// Simpler counter server.
type Counter int
func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
*ctr++
fmt.Fprintf(w, "counter = %d\n", *ctr)
}
如果你的程序有一些内部状态,需要通知你有一个页面被访问了怎么办?给网页绑定一个频道。
// A channel that sends a notification on each visit.
// (Probably want the channel to be buffered.)
type Chan chan *http.Request
func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ch <- req
fmt.Fprint(w, "notification sent")
}
最后,假设我们想在/args上显示调用服务器二进制时使用的参数。很容易写一个函数来打印参数。
func ArgServer() {
fmt.Println(os.Args)
}
我们如何把它变成一个HTTP服务器?我们可以让ArgServer成为某个类型的方法,我们忽略它的值,但是有一个更干净的方法。因为除了指针和接口,我们可以为任何类型定义一个方法,我们可以为一个函数写一个方法。http包中包含了这样的代码。
// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler object that calls f.
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, req).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
f(w, req)
}
HandlerFunc是一个带方法的类型,ServeHTTP,所以该类型的值可以服务于HTTP请求。看看方法的实现:接收者是一个函数f,方法调用f,这看起来很奇怪,但这和比如说,接收者是一个通道,方法在通道上发送并没有什么不同。
为了使ArgServer成为一个HTTP服务器,我们首先要修改它的签名,使其具有正确的签名。
// Argument server.
func ArgServer(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, os.Args)
}
现在ArgServer与HandlerFunc具有相同的签名,因此可以将其转换为该类型来访问其方法,就像我们将Sequence转换为IntSlice来访问IntSlice.Sort一样。设置它的代码很简洁。
http.Handle("/args", http.HandlerFunc(ArgServer))
当有人访问/args页面时,安装在该页面的处理程序的值为ArgServer,类型为HandlerFunc。HTTP服务器将调用该类型的ServeHTTP方法,ArgServer作为接收方,而接收方又会调用ArgServer(通过HandlerFunc.ServeHTTP内部的调用f(w, req))。然后,参数将被显示出来。
在这一节中,我们从一个结构、一个整数、一个通道和一个函数制作了一个HTTP服务器,这都是因为接口只是方法的集合,它可以为(几乎)任何类型定义。
3.3 - 接口实现
go语言实战
隐式接口
类型通过实现那些方法来实现接口。 没有显式声明的必要;所以也就没有关键字“implements“。
隐式接口解藕了实现接口的包和定义接口的包:互不依赖。
因此,也就无需在每一个实现上增加新的接口名称。
常见的接口:
type Stringer interface {
String() string
}
Stringer
是一个可以用字符串描述自己的类型。fmt
包 (还有许多其他包)使用这个来进行输出。
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}
Effective Go
https://golang.org/doc/effective_go.html#interfaces
Go中的接口提供了一种指定对象行为的方法:如果某个东西可以做这个,那么它就可以在这里使用。我们已经看到了几个简单的例子;自定义的打印机可以通过一个String方法实现,而Fprintf可以通过一个Write方法向任何东西生成输出。只有一个或两个方法的接口在Go代码中很常见,通常会被赋予一个由方法派生的名称,比如io.Writer用于实现Write方法的东西。
一个类型可以实现多个接口。例如,一个集合如果实现了 sort.Interface,就可以通过包 sort 中的例程进行排序,其中包含 Len()、Less(i, j int) bool 和 Swap(i, j int),它还可以有一个自定义的格式器。在这个人为的例子中,Sequence同时满足这两个条件。
type Sequence []int
// Methods required by sort.Interface.
func (s Sequence) Len() int {
return len(s)
}
func (s Sequence) Less(i, j int) bool {
return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// Copy returns a copy of the Sequence.
func (s Sequence) Copy() Sequence {
copy := make(Sequence, 0, len(s))
return append(copy, s...)
}
// Method for printing - sorts the elements before printing.
func (s Sequence) String() string {
s = s.Copy() // Make a copy; don't overwrite argument.
sort.Sort(s)
str := "["
for i, elem := range s { // Loop is O(N²); will fix that in next example.
if i > 0 {
str += " "
}
str += fmt.Sprint(elem)
}
return str + "]"
}
3.4 - 接口检查
Effective Go
接口检查
https://golang.org/doc/effective_go.html#blank_implements
正如我们在上面关于接口的讨论中所看到的,类型不需要明确声明它实现了接口。相反,类型只要实现了接口的方法就实现了接口。在实践中,大多数接口转换都是静态的,因此在编译时进行检查。例如,将一个 *os.File 传给一个期望有 io.Reader 的函数,除非 *os.File 实现了 io.Reader 接口,否则不会被编译。
但有些接口检查确实是在运行时进行的。其中一个例子是在 encoding/json 包中,它定义了一个 Marshaler 接口。当JSON编码器接收到一个实现该接口的值时,编码器会调用该值的 marshaling 方法将其转换为JSON,而不是进行标准转换。编码器在运行时用一个类型断言(type assertion)检查这个属性,比如。
m, ok := val.(json.Marshaler)
如果只需要询问一个类型是否实现了一个接口,而不实际使用接口本身,或许作为错误检查的一部分,使用空白标识符来忽略类型假定的值。
if _, ok := val.(json.Marshaler); ok {
fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}
出现这种情况的一个地方是,当需要在实现类型的包内保证它确实满足接口的时候。如果一个类型–例如,json.RawMessag–需要自定义的JSON表示,它应该实现 json.Marshaler,但没有静态转换会导致编译器自动验证这一点。如果该类型无意中没有满足接口,JSON编码器仍然会工作,但不会使用自定义的实现。为了保证实现的正确性,可以在包中使用空白标识符的全局声明。
var _ json.Marshaler = (*RawMessage)(nil)
在这个声明中,涉及到将RawMessage转换为Marshaler的赋值,需要RawMessage实现Marshaler,并且在编译时将检查该属性。如果json.Marshaler接口发生变化,这个包将不再编译,我们会被通知需要更新。
在这个构造中出现空白标识符,说明声明的存在只是为了类型检查,而不是为了创建一个变量。不过不要对每个满足接口的类型都这样做。按照惯例,这种声明只有在代码中已经没有静态转换时才会使用,这是很罕见的事件。
3.5 - type switch语句
type switch语句的语法详情见:switch 语句
Effective Go
转换
https://golang.org/doc/effective_go.html#conversions
Sequence的String方法重现了Sprint已经为slices做的工作。(它的复杂度也是O(N²),这很差。) 如果我们在调用Sprint之前将Sequence转换为一个普通的[]int,我们就可以分担这项工作(也可以加快它的速度)。
func (s Sequence) String() string {
s = s.Copy()
sort.Sort(s)
return fmt.Sprint([]int(s))
}
这个方法是另一个从String方法安全调用Sprintf的转换技术的例子。因为两个类型(Sequence和[]int)是一样的,如果我们忽略类型名,那么在它们之间进行转换是合法的。这个转换并没有创建一个新的值,它只是暂时把现有的值当作一个新的类型。(还有其他合法的转换,比如从整数到浮点,确实会创建一个新的值。)
这是Go程序中的一个习惯做法,用来转换表达式的类型以访问不同的方法集。举个例子,我们可以使用现有的 sort.IntSlice 类型,将整个例子简化为这样。
type Sequence []int
// Method for printing - sorts the elements before printing
func (s Sequence) String() string {
s = s.Copy()
sort.IntSlice(s).Sort()
return fmt.Sprint([]int(s))
}
现在,我们不再让Sequence实现多个接口(排序和打印),而是利用一个数据项转换为多种类型(Sequence、sort.IntSlice和[]int)的能力,每种类型都能完成一部分工作。这在实践中比较少见,但可以很有效。
接口转换和类型断言
https://golang.org/doc/effective_go.html#interface_conversions
type switch 是一种转换形式:它们接受一个接口,并在某种意义上,对于switch中的每一个case,将其转换为该case的类型。下面是fmt.Printf下的代码如何使用类型转换将一个值变成一个字符串的简化版本。如果它已经是一个字符串,我们要的是接口所持有的实际字符串值,而如果它有一个String方法,我们要的是调用该方法的结果。
type Stringer interface {
String() string
}
var value interface{} // Value provided by caller.
switch str := value.(type) {
case string:
return str
case Stringer:
return str.String()
}
第一个case是找到一个具体的值;第二个case是将接口转换成另一个接口。这样混合类型是完全可以的。
如果我们只关心一种类型呢?如果我们知道这个值持有一个字符串,而我们只想提取它?一个one-case类型switch就可以了,但类型断言(type assertion)也可以。类型断言接受一个接口值,并从中提取一个指定显式类型的值。语法借鉴了打开类型转换的子句,但用的是显式类型而不是类型关键字。
value.(typeName)
结果是一个静态类型typeName的新值。该类型必须是接口所持有的具体类型,或者是该值可以转换为的第二个接口类型。为了提取我们知道的值中的字符串,我们可以写。
str := value.(string)
但如果发现值不包含字符串,程序就会因运行时错误而崩溃。为了防止这种情况,可以使用 “comma, ok” 这个习惯用法来安全地测试值是否是字符串。
str, ok := value.(string)
if ok {
fmt.Printf("string value is: %q\n", str)
} else {
fmt.Printf("value is not a string\n")
}
如果类型断言失败,str仍将存在,并且类型为string,但它的值为零,是一个空字符串。
为了说明这种能力,这里有一个if-else语句,相当于本节开头的类型切换。
if str, ok := value.(string); ok {
return str
} else if str, ok := value.(Stringer); ok {
return str.String()
}
3.6 - 接口嵌入
Effective Go
https://golang.org/doc/effective_go.html#embedding
Go并没有提供典型的、类型驱动的子类概念,但它确实可以通过在结构体或接口中嵌入类型来 “借用"实现的一部分。
接口嵌入非常简单。我们之前已经提到了 io.Reader 和 io.Writer 接口,下面是它们的定义。
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
io包还导出了其他一些接口,这些接口指定了可以实现若干这样方法的对象。例如,有io.ReadWriter,一个包含Read和Write的接口。我们可以通过显式列出这两个方法来指定io.ReadWriter,但像这样把这两个接口嵌入形成新的接口,会更容易,也更有感召力。
// ReadWriter is the interface that combines the Reader and Writer interfaces.
type ReadWriter interface {
Reader
Writer
}
如代码所示: ReadWriter可以做Reader做的事情,也可以做Writer做的事情;它是一个嵌入接口(必须是不相干的方法集)的联合体。只有接口可以嵌入到接口中。
同样的基本思想也适用于结构体,但其影响更为深远。bufio包有两个结构类型,bufio.Reader和bufio.Writer,当然每个结构都实现了包io中的类似接口。而且bufio还实现了一个缓冲的读/写器,它是通过使用嵌入的方式将一个读器和一个写器合并到一个结构中来实现的:它列出了结构中的类型,但没有给它们起字段名:
// ReadWriter stores pointers to a Reader and a Writer.
// It implements io.ReadWriter.
type ReadWriter struct {
*Reader // *bufio.Reader
*Writer // *bufio.Writer
}
嵌入的元素是指向结构体的指针,当然在使用之前必须初始化为指向有效的结构。ReadWriter结构可以写成:
type ReadWriter struct {
reader *Reader
writer *Writer
}
但这样一来,为了提升字段的方法以满足io接口,我们还需要提供转发方法,比如这样:
func (rw *ReadWriter) Read(p []byte) (n int, err error) {
return rw.reader.Read(p)
}
通过直接嵌入结构,我们避免了这种方式。嵌入类型的方法直接附加,这意味着bufio.ReadWriter不仅拥有bufio.Reader和bufio.Writer的方法,还满足了所有三个接口:io.Reader、io.Writer和io.ReadWriter。
嵌入和子类有一个重要的区别。当我们嵌入一个类型时,该类型的方法会成为外部类型的方法,但当它们被调用时,方法的接收者是内部类型,而不是外部类型。在我们的例子中,当调用一个bufio.ReadWriter的Read方法时,它的效果和上面写出来的转发方法完全一样,接收者是ReadWriter的reader字段,而不是ReadWriter本身。
嵌入也可以是一种简单的方便。这个例子显示了一个嵌入字段与一个常规的、命名的字段并列。
type Job struct {
Command string
*log.Logger
}
现在,Job类型有了*log.Logger的Print、Printf、Println等方法。当然,我们可以给Logger取一个字段名,但没有必要这么做。而现在,一旦初始化,我们就可以将日志记录到Job中。
job.Println("starting now...")
Logger是Job结构的一个常规字段,所以我们可以在Job的构造函数中以通常的方式初始化它,像这样:
func NewJob(command string, logger *log.Logger) *Job {
return &Job{command, logger}
}
或用组合字面量:
job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}
如果我们需要直接引用一个嵌入的字段,那么字段的类型名,忽略包的限定符,作为字段名,就像在我们的ReadWriter结构的Read方法中一样。在这里,如果我们需要访问一个Job变量job的*log.Logger,我们会写job.Logger,如果我们想完善Logger的方法,这将是非常有用的。
func (job *Job) Printf(format string, args ...interface{}) {
job.Logger.Printf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}
嵌入类型引入了名称冲突的问题,但解决这些问题的规则很简单。首先,一个字段或方法X将任何其他项目X隐藏在类型的更深嵌套部分。如果log.Logger包含一个名为Command的字段或方法,那么Job的Command字段就会支配它。
其次,如果相同的名称出现在相同的嵌套层次,通常是一个错误;如果Job结构包含另一个名为Logger的字段或方法,那么嵌入log.Logger将是错误的。但是,如果重复的名称在程序中从未在类型定义之外提及,则是可以的。这个限定提供了一些保护,防止从外部对嵌入的类型进行修改;如果添加的字段与另一个子类型中的另一个字段发生冲突,如果两个字段都没有使用过,那么就没有问题。
4 - 错误处理
4.1 - 错误处理概述
go by example
https://gobyexample-cn.github.io/errors
符合 Go 语言习惯的做法是使用一个独立、明确的返回值来传递错误信息。
这与 Java、Ruby 使用的异常(exception) 以及在 C 语言中有时用到的重载 (overloaded) 的单返回/错误值有着明显的不同。
Go 语言的处理方式能清楚的知道哪个函数返回了错误,并使用跟其他(无异常处理的)语言类似的方式来处理错误。
// 按照惯例,错误通常是最后一个返回值并且是 error 类型,它是一个内建的接口。
func f1(arg int) (int, error) { //
if arg == 42 {
// errors.New 使用给定的错误信息构造一个基本的 error 值。
return -1, errors.New("can't work with 42")
}
// 返回错误值为 nil 代表没有错误。
return arg + 3, nil
}
你还可以通过实现 Error() 方法来自定义 error 类型。 这里使用自定义错误类型来表示上面例子中的参数错误:
// 使用自定义错误类型来表示上面例子中的参数错误
type argError struct {
arg int
prob string
}
// 通过实现 Error() 方法来自定义 error 类型
func (e *argError) Error() string {
return fmt.Sprintf("%d - %s", e.arg, e.prob)
}
func f2(arg int) (int, error) {
if arg == 42 {
// 使用 &argError 语法来建立一个新的结构体
// 并提供了 arg 和 prob 两个字段的值
return -1, &argError{arg, "can't work with it"}
}
return arg + 3, nil
}
Effective Go
https://golang.org/doc/effective_go.html#errors
类库例程必须经常向调用者返回某种错误指示。如前所述,Go的多值返回使得在返回正常返回值的同时很容易返回一个详细的错误说明。利用这个特性来提供详细的错误信息是一种很好的风格。例如,正如我们将看到的,os.Open 并不只是在失败时返回一个 nil 指针,它还返回一个错误值来描述出了什么问题。
按照惯例,错误的类型为error,是一个简单的内置接口。
type error interface {
Error() string
}
类库编写者可以自由地用更丰富的模型来实现这个接口,使其不仅可以看到错误,还可以提供一些上下文。如前所述,除了通常的*os.File返回值之外,os.Open还返回一个错误值。如果文件被成功打开,错误值将为nil,但当出现问题时,它将持有一个os.PathError。
// PathError records an error and the operation and
// file path that caused it.
type PathError struct {
Op string // "open", "unlink", etc.
Path string // The associated file.
Err error // Returned by the system call.
}
func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": " + e.Err.Error()
}
PathError的Error会生成这样一个字符串:
open /etc/passwx: no such file or directory
这样的错误包括有问题的文件名、操作和它所触发的操作系统错误,即使打印出来的时候离引起它的调用很远也是有用的;它比单纯的 “没有这样的文件或目录 “信息量大得多。
在可行的情况下,错误字符串应该识别它们的来源,例如通过有一个前缀来命名产生错误的操作或包。例如,在包image中,由于未知格式导致的解码错误的字符串表示是 “image: unknown format”。
关心精确错误细节的调用者可以使用类型开关(type switch)或类型断言(type assertion)来查找特定错误并提取细节。对于PathErrors来说,这可能包括检查内部Err字段是否存在可恢复的故障。
for try := 0; try < 2; try++ {
file, err = os.Create(filename)
if err == nil {
return
}
if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
deleteTempFiles() // Recover some space.
continue
}
return
}
这里的第二个 if 语句是另一种类型的断言。如果它失败了,ok将为false,e将为nil. 如果它成功了,ok将为true,这意味着错误类型为*os.PathError,那么e也是如此,我们可以检查更多信息。
4.2 - [博客] 错误处理与go
备注:golang官方blog文章 Error handling and Go
介绍
只要写过任何Go代码,就可能遇到过内置的 error 类型。Go代码使用错误值来表示异常状态。例如,os.Open函数在打开文件失败时,会返回一个非零的错误值:
func Open(name string) (file *File, err error)
下面的代码使用 os.Open 来打开一个文件,如果发生错误,则调用 log.Fatal 来打印错误信息并停止。
f, err := os.Open("filename.ext")
if err != nil {
log.Fatal(err)
}
// do something with the open *File f
只知道错误类型这一点,你就可以在Go中完成很多工作,但在这篇文章中,我们将仔细研究错误,并讨论一些在Go中处理错误的好做法。
error 类型
error 类型是一种接口类型。error 变量代表任何可以描述为字符串的值。下面是接口的声明。
type error interface {
Error() string
}
与所有内置类型一样,error 类型在宇宙块中预先声明。
最常用的 error 实现是 error package 的未导出的 errorString 类型。
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
可以使用 errors.New 函数来构造这些值。它接收一个字符串,并将其转换为 errors.errorString,然后作为一个错误值返回。
// New returns an error that formats as the given text.
func New(text string) error {
return &errorString{text}
}
下面是如何使用 errors.New:
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
// implementation
}
传递负参数给Sqrt的调用者会收到一个非零的错误值(其具体表示是一个error.errorString值)。调用者可以通过调用错误的Error方法来访问错误字符串(“math: square root of…"),或者直接打印它。
f, err := Sqrt(-1)
if err != nil {
fmt.Println(err)
}
fmt包通过调用 Error() 字符串方法来格式化错误值。
错误实现的责任是总结上下文。os.Open 返回的错误格式为 “open /etc/passwd: permission denied”,而不仅仅是 “permission denied."。我们的Sqrt返回的错误缺少了无效参数的信息。
要添加这些信息,一个有用的函数是fmt包的Errf。它根据Printf的规则格式化一个字符串,并将其作为一个由error.New创建的错误返回。
if f < 0 {
return 0, fmt.Errorf("math: square root of negative number %g", f)
}
在很多情况下,fmt.Errorf已经足够好了,但是由于error是一个接口,你可以使用任意的数据结构作为错误值,以允许调用者检查错误的细节。
例如,我们假设的调用者可能想恢复传递给Sqrt的无效参数。我们可以通过定义一个新的错误实现而不是使用 errors.errorString 来实现。
type NegativeSqrtError float64
func (f NegativeSqrtError) Error() string {
return fmt.Sprintf("math: square root of negative number %g", float64(f))
}
然后,复杂的调用者可以使用类型断言来检查NegativeSqrtError,并对其进行特殊处理,而只是将错误传递给fmt.Println或log.Fatal的调用者则不会看到行为的改变。
另一个例子是,json包指定了一个SyntaxError类型,当json.Decode函数在解析JSON blob时遇到语法错误时,它会返回这个类型。
type SyntaxError struct {
msg string // description of error
Offset int64 // error occurred after reading Offset bytes
}
func (e *SyntaxError) Error() string { return e.msg }
Offset字段甚至没有显示在错误的默认格式中,但调用者可以使用它来为他们的错误信息添加文件和行信息。
if err := dec.Decode(&val); err != nil {
if serr, ok := err.(*json.SyntaxError); ok {
line, col := findLine(f, serr.Offset)
return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err)
}
return err
}
这是Camlistore项目中一些实际代码的略微简化版本)。
错误接口只需要一个Error方法;特定的错误实现可能有额外的方法。例如,net包按照通常的惯例返回类型为error的错误,但一些错误实现有net.Error接口定义的附加方法。
package net
type Error interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}
客户端代码可以通过类型断言来测试net.Error,然后区分暂时性的网络错误和永久性的错误。例如,网络爬虫可能会在遇到暂时性错误时休眠并重试,否则就会放弃。
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
time.Sleep(1e9)
continue
}
if err != nil {
log.Fatal(err)
}
简化重复性错误处理
在Go中,错误处理很重要。该语言的设计和约定鼓励您在错误发生时明确地检查错误(与其他语言中的抛出异常和有时捕获错误的约定不同)。在某些情况下,这使得Go代码变得啰嗦,但幸运的是,您可以使用一些技术来减少重复的错误处理。
考虑一个带有 HTTP 处理程序的 App Engine 应用程序,该处理程序从数据存储中检索一条记录,并使用模板对其进行格式化。
func init() {
http.HandleFunc("/view", viewRecord)
}
func viewRecord(w http.ResponseWriter, r *http.Request) {
c := appengine.NewContext(r)
key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
record := new(Record)
if err := datastore.Get(c, key, record); err != nil {
http.Error(w, err.Error(), 500)
return
}
if err := viewTemplate.Execute(w, record); err != nil {
http.Error(w, err.Error(), 500)
}
}
这个函数处理由datastore.Get函数和viewTemplate的Execute方法返回的错误。在这两种情况下,它都会向用户呈现一个简单的错误信息,并给出HTTP状态码500(“内部服务器错误”)。这看起来是一个可管理的代码量,但增加一些HTTP处理程序,你很快就会得到许多相同错误处理代码的副本。
为了减少重复,我们可以定义自己的HTTP appHandler类型,其中包括一个错误返回值:
type appHandler func(http.ResponseWriter, *http.Request) error
然后,我们可以改变我们的viewRecord函数来返回错误。
func viewRecord(w http.ResponseWriter, r *http.Request) error {
c := appengine.NewContext(r)
key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
record := new(Record)
if err := datastore.Get(c, key, record); err != nil {
return err
}
return viewTemplate.Execute(w, record)
}
这比原来的版本更简单,但http包并不理解返回错误的函数。为了解决这个问题,我们可以在appHandler上实现http.Handler接口的ServeHTTP方法。
func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := fn(w, r); err != nil {
http.Error(w, err.Error(), 500)
}
}
ServeHTTP方法调用appHandler函数,并向用户显示返回的错误(如果有的话)。请注意,该方法的接收器 fn 是一个函数,(Go 可以做到这一点!)该方法通过调用表达式 fn(w) 中的接收者来调用函数。(Go 可以做到这一点!) 方法通过调用表达式 fn(w, r) 中的接收者来调用函数。
现在,当我们在http包中注册viewRecord时,我们使用Handle函数(而不是HandleFunc),因为appHandler是一个http.Handler(而不是http.HandlerFunc)。
func init() {
http.Handle("/view", appHandler(viewRecord))
}
有了这个基本的错误处理基础架构,我们可以让它变得更加友好。与其仅仅显示错误字符串,不如给用户一个简单的错误信息,并附上适当的HTTP状态代码,同时将完整的错误记录到App Engine开发者控制台,以便进行调试。
为此,我们创建一个appError结构,包含一个错误和一些其他字段。
type appError struct {
Error error
Message string
Code int
}
接下来我们修改appHandler类型来返回*appError值。
type appHandler func(http.ResponseWriter, *http.Request) *appError
(通常情况下,传回 error 的具体类型而不是 error 是错误的,原因在Go FAQ中讨论过,但在这里是正确的,因为ServeHTTP是唯一能看到该值并使用其内容的地方。)
并让appHandler的ServeHTTP方法将appError的Message以正确的HTTP状态码显示给用户,并将完整的Error记录到开发者控制台。
func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if e := fn(w, r); e != nil { // e is *appError, not os.Error.
c := appengine.NewContext(r)
c.Errorf("%v", e.Error)
http.Error(w, e.Message, e.Code)
}
}
最后,我们将viewRecord更新为新的函数签名,并让它在遇到错误时返回更多的上下文。
func viewRecord(w http.ResponseWriter, r *http.Request) *appError {
c := appengine.NewContext(r)
key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
record := new(Record)
if err := datastore.Get(c, key, record); err != nil {
return &appError{err, "Record not found", 404}
}
if err := viewTemplate.Execute(w, record); err != nil {
return &appError{err, "Can't display record", 500}
}
return nil
}
这个版本的viewRecord和原来的长度是一样的,但现在每一行都有特定的意义,我们提供的是更友好的用户体验。
这并没有结束,我们还可以进一步改进我们应用程序中的错误处理。一些想法。
-
给错误处理程序一个漂亮的HTML模板。
-
当用户是管理员时,通过将堆栈跟踪写入HTTP响应,使调试变得更容易。
-
为appError写一个构造函数,存储堆栈跟踪以方便调试。
-
从appHandler内部的恐慌中恢复,将错误记录到控制台中,称为 “Critical”,同时告诉用户 “发生了一个严重的错误”。这是一个很好的触动,避免了让用户暴露在编程错误引起的不可捉摸的错误信息中。更多细节请参见Defer、Panic和Recover文章。
结束语
正确的错误处理是优秀软件的基本要求。通过运用本篇文章中描述的技术,你应该能够写出更可靠、更简洁的Go代码。
4.3 - [博客] 错误是值
备注:golang官方blog文章 Errors are values
在Go程序员中,尤其是那些刚接触这门语言的程序员,经常讨论的一个问题就是如何处理错误。谈话往往变成了对这个序列的次数的哀叹:
if err != nil {
return err
}
显示出来。我们最近扫描了所有能找到的开源项目,发现这个片段每一两页只出现一次,比一些人认为的要少。不过,如果人们仍然认为必须键入:
if err != nil
一直以来,一定有什么地方出了问题,而明显的目标是 go 本身。
这是不幸的,误导性的,而且很容易纠正。也许发生的情况是,刚接触 go 的程序员会问:“如何处理错误?"。学会了这种模式,就止步于此。在其他语言中,人们可能会使用 try-catch 块或其他类似机制来处理错误。因此,程序员就会想,我在以前的语言中会使用 try-catch,在Go中我就直接输入 if err != nil。随着时间的推移,Go代码收集了很多这样的片段,结果感觉很笨拙。
不管这种解释是否合适,很明显,这些Go程序员忽略了一个关于错误的基本点:Errors are values (错误是值)。
值可以被编程,既然错误是值,那么错误也可以被编程。
当然,涉及错误值的常见语句是测试它是否为nil,但还有无数其他的事情可以用错误值来做,应用这些其他的一些事情可以让你的程序变得更好,消除了很多如果每个错误都用死板的 if 语句来检查所产生的模板。
下面是一个简单的例子,来自 bufio 包的 Scanner 类型。它的 Scan 方法执行了底层的I/O,这当然会导致错误。然而Scan方法根本没有暴露错误。相反,它返回一个布尔值,并在扫描结束时运行一个单独的方法,报告是否发生错误。客户端代码是这样的。
scanner := bufio.NewScanner(input)
for scanner.Scan() {
token := scanner.Text()
// process token
}
if err := scanner.Err(); err != nil {
// process the error
}
当然,有一个错误的nil检查,但它只出现和执行一次。扫描方法可以被定义为:
func (s *Scanner) Scan() (token []byte, error)
然后示例用户代码可能是(取决于如何检索令牌):
scanner := bufio.NewScanner(input)
for {
token, err := scanner.Scan()
if err != nil {
return err // or maybe break
}
// process token
}
这并没有什么不同,但有一个重要的区别。在这段代码中,客户端必须在每次迭代时检查错误,但在真正的 Scanner API 中,错误处理是从关键的 API 元素中抽象出来的,而关键的 API 元素就是迭代 tokens。因此,使用真正的API,客户端的代码感觉更自然:循环直到完成,然后再担心错误。错误处理不会掩盖控制流。
当然,在掩盖之下发生的事情是,一旦Scan遇到一个I/O错误,它就会记录下来并返回false。当客户端询问时,一个单独的方法Err会报告错误值。虽然这很微不足道,但它和把
if err != nil
放的遍地都是,或者要求客户端在每个token之后检查错误。这就是带有错误值的编程。简单的编程,是的,但还是编程。
值得强调的是,无论设计如何,程序检查错误是至关重要的,无论它们是如何暴露的。这里讨论的不是如何避免检查错误,而是如何使用语言优雅地处理错误。
当我参加2014年秋季在东京举行的GoCon时,就出现了重复查错代码的话题。一位在Twitter上化名为@jxck_的热心地鼠对错误检查发出了熟悉的感叹。他有一些代码的示意是这样的。
_, err = fd.Write(p0[a:b])
if err != nil {
return err
}
_, err = fd.Write(p1[c:d])
if err != nil {
return err
}
_, err = fd.Write(p2[e:f])
if err != nil {
return err
}
// and so on
这是非常重复的。在真实的代码中,这段代码比较长,发生的事情比较多,所以不容易只用帮助函数重构,但在这种理想化的形式下,在错误变量上关联一个函数字面量会有帮助:
var err error
write := func(buf []byte) {
if err != nil {
return
}
_, err = w.Write(buf)
}
write(p0[a:b])
write(p1[c:d])
write(p2[e:f])
// and so on
if err != nil {
return err
}
这种模式很好用,但需要在每个做write的函数中都有一个闭包;单独的帮助函数使用起来比较笨拙,因为err变量需要在不同的调用中维护(试试)。
我们可以借鉴上面Scan方法的思想,使之更干净、更通用、更可重用。我在我们的讨论中提到了这个技术,但@jxck_并没有看到如何应用它。经过长时间的交流,由于语言障碍,我问是否可以借用他的笔记本,通过输入一些代码给他看。
我定义了一个叫 errWriter 的对象,类似这样。
type errWriter struct {
w io.Writer
err error
}
并给了它一个方法,write。它不需要有标准的 Write 签名,而且它的小写部分是为了突出区别。write方法会调用底层Writer的Write方法,并记录第一个错误,供以后参考。
func (ew *errWriter) write(buf []byte) {
if ew.err != nil {
return
}
_, ew.err = ew.w.Write(buf)
}
一旦发生错误,写方法就会变成无操作,但错误值会被保存。
给定errWriter类型和它的写法,上面的代码可以重构。
ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// and so on
if ew.err != nil {
return ew.err
}
这样做更干净,甚至比使用闭包更干净,也使实际的写入顺序更容易在页面上看到。再也没有杂乱无章的东西了。用错误值(和接口)编程让代码变得更漂亮了。
很有可能在同一个包中的其他代码可以建立在这个想法上,甚至直接使用errWriter。
另外,一旦errWriter存在,它还可以做更多的事情来帮助我们,尤其是在不太人为的例子中。它可以累积字节数。它可以将写入的内容凝聚成一个单一的缓冲区,然后可以原子化地传输。还有更多。
事实上,这种模式经常出现在标准库中。archive/zip和net/http包都使用了它。更突出的是,bufio包的Writer实际上是errWriter思想的一个实现。虽然bufio.Writer.Write会返回一个错误,但那主要是为了尊重io.Writer接口。bufio.Writer的Write方法的行为就像我们上面的errWriter.write方法一样,Flush会报告错误,所以我们的例子可以这样写。
b := bufio.NewWriter(fd)
b.Write(p0[a:b])
b.Write(p1[c:d])
b.Write(p2[e:f])
// and so on
if b.Flush() != nil {
return b.Flush()
}
这种方法有一个重大的缺点,至少对某些应用来说是这样:无法知道错误发生前完成了多少处理。如果该信息很重要,就需要采用更细化的方法。不过,通常情况下,在最后进行全有或全无的检查就足够了。
我们只看了一种避免重复性错误处理代码的技术。请记住,使用 errWrite r或 bufio.Writer 并不是简化错误处理的唯一方法,而且这种方法并不适合所有情况。然而,关键的经验是,错误是值,可以利用Go编程语言的全部能力来处理它们。
使用该语言来简化你的错误处理。
但请记住。无论你做什么,总是要检查你的错误!
4.4 - [博客] go1.13中的错误处理
备注:golang官方blog文章 Working with Errors in Go 1.13
介绍
在过去的十年里,Go 将错误处理为值(errors as values)的做法对我们很有帮助。虽然标准库对错误的支持很少–只有 errors.New 和 fmt.Errorf 函数,它们产生的错误只包含一条消息–但内置的 error 接口允许 Go 程序员添加他们想要的任何信息。它所需要的只是一个实现 Error 方法的类型。
type QueryError struct {
Query string
Err error
}
func (e *QueryError) Error() string { return e.Query + ": " + e.Err.Error() }
像这样的错误类型无处不在,它们所存储的信息也千差万别,从时间戳到文件名到服务器地址。通常,这些信息包括另一个低级错误,以提供额外的上下文。
一个错误包含另一个错误的模式在 Go 代码中是如此普遍,以至于经过广泛的讨论,Go 1.13 增加了对它的明确支持。这篇文章描述了标准库中提供这种支持的新增内容:错误包中的三个新函数,以及 fmt.Errorf 的新格式动词。
在详细描述这些变化之前,让我们先回顾一下在以前的语言版本中是如何检查和构造错误的。
go 1.13之前的错误
检查错误
Go error 是值。程序根据这些值以几种方式做出决定。最常见的是将错误与nil进行比较,以确定操作是否失败。
if err != nil {
// something went wrong
}
有时我们会将错误与已知的 sentinel 值进行比较,看看是否发生了特定的错误。
var ErrNotFound = errors.New("not found")
if err == ErrNotFound {
// something wasn't found
}
错误值可以是满足语言定义的 error 接口的任何类型。程序可以使用类型断言或类型转换来将错误值视为更具体的类型。
type NotFoundError struct {
Name string
}
func (e *NotFoundError) Error() string { return e.Name + ": not found" }
if e, ok := err.(*NotFoundError); ok {
// e.Name wasn't found
}
添加信息
经常有函数在调用堆栈中传递错误,同时在其中添加信息,比如错误发生时的简要描述。一个简单的方法是构造新的错误,其中包括前一个错误的文本。
if err != nil {
return fmt.Errorf("decompress %v: %v", name, err)
}
使用 fmt.Errorf 创建一个新的错误,会丢弃原始错误中除文本以外的所有内容。正如我们在上面的QueryError中所看到的,我们有时可能希望定义一个新的错误类型,其中包含底层错误,保留它以便于代码检查。这里又是QueryError。
type QueryError struct {
Query string
Err error
}
程序可以在 *QueryError
值内部查看,根据底层 error 做出决定。你有时会看到这被称为 “解包 “错误。
if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
// query failed because of a permission problem
}
标准库中的os.PathError类型是另一个例子,它包含了一个错误。
Errors in Go 1.13
Unwrap 方法
Go 1.13 为 errors 和 fmt 标准库包引入了新特性,以简化对包含其他错误的错误的处理。其中最重要的是约定,而不是变化:包含另一个error的 error 可以实现一个返回底层 error 的 Unwrap 方法。如果 e1.Unwrap() 返回 e2,那么我们就说 e1 封装(wrap)了e2,可以解开 (unwrap) e1 得到 e2。
按照这个约定,我们可以给上面的 QueryError 类型一个 Unwrap 方法,返回其包含的错误。
func (e *QueryError) Unwrap() error { return e.Err }
使用 Is 和 As 检查错误
Go 1.13 errors 包包含了两个新的错误检查函数:Is 和 As。
errors.Is 函数将错误与值进行比较:
// Similar to:
// if err == ErrNotFound { … }
if errors.Is(err, ErrNotFound) {
// something wasn't found
}
As 函数测试 error 是否为特定类型:
// Similar to:
// if e, ok := err.(*QueryError); ok { … }
var e *QueryError
if errors.As(err, &e) {
// err is a *QueryError, and e is set to the error's value
}
在最简单的情况下,errors.Is 函数的行为就像与 sentinel error 的比较,而 errors.As 函数的行为就像类型断言。然而,当对封装的错误进行操作时,这些函数会考虑链中的所有error。让我们再看看上面的例子,即解开一个 QueryError 来检查底层错误。
if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
// query failed because of a permission problem
}
使用 errors.Is 函数,我们可以把它写成:
if errors.Is(err, ErrPermission) {
// err, or some error that it wraps, is a permission problem
}
errors 包还包括一个新的 Unwrap 函数,它返回调用 error 的 Unwrap 方法的结果,如果错误没有Unwrap方法,则返回 nil。但通常最好使用 errors.Is 或 errors.As,因为这些函数会在一次调用中检查整个链。
使用 %w 封装错误
如前所述,通常使用 fmt.Errorf 函数为错误添加附加信息。
if err != nil {
return fmt.Errorf("decompress %v: %v", name, err)
}
在Go 1.13中,fmt.Errorf 函数支持一个新的 %w 动词。当这个动词存在时,fmt.Errorf 返回的错误将有一个 Unwrap 方法返回 %w 的参数,它必须是一个error。在所有其他方面,%w 与 %v 相同:
if err != nil {
// Return an error which unwraps to err.
return fmt.Errorf("decompress %v: %w", name, err)
}
用 %w 包装 error ,使得它可以被 errors.Is 和 erros.As 使用。
err := fmt.Errorf("access denied: %w", ErrPermission)
...
if errors.Is(err, ErrPermission) ...
是否封装
当向 error 添加额外的上下文时,无论是使用 fmt.Errorf 还是通过实现自定义类型,您都需要决定新的 error 是否应该封装原始 error。这个问题没有唯一的答案,它取决于创建新 error 的上下文。封装一个error 以将其暴露给调用者。如果这样做会暴露实现细节,则不要包装error。
举个例子,想象一个从 io.Reader.Reader 中读取复杂数据结构的 Parsse 函数。如果发生错误,我们希望报告发生错误的行号和列号。如果错误是在从 io.Reader 读取时发生的,我们希望对该错误进行包装,以便检查潜在的问题。由于调用者向函数提供了 io.Reader,所以暴露它所产生的错误是有意义的。
相反,一个对数据库进行多次调用的函数可能不应该返回一个对其中一次调用结果进行解包的错误。如果函数使用的数据库是一个实现细节,那么暴露这些错误就违反了抽象性。例如,如果你的包pkg的LookupUser函数使用了Go的数据库/sql包,那么它可能会遇到一个sql.ErrNoRows错误。如果你用fmt.Errorf(“accessing DB: %v”, err)来返回这个错误,那么调用者就不能在里面查找sql.ErrNoRows。但是如果函数返回的是fmt.Errorf(“accessing DB: %w”, err),那么调用者就可以合理地写道
err := pkg.LookupUser(...)
if errors.Is(err, sql.ErrNoRows) …
这时,如果你不想破坏你的客户端,即使你切换到不同的数据库包,函数必须总是返回 sql.ErrNoRows 。换句话说,包装一个错误使该错误成为你的API的一部分。如果你不想承诺在未来支持该错误作为你的API的一部分,你就不应该包装该错误。
重要的是要记住,无论你是否封装,错误文本都是一样的。试图理解该错误的人无论用哪种方式都会得到相同的信息;选择封装是为了给程序提供额外的信息,以便他们能够做出更明智的决定,还是为了保留抽象层而不提供该信息。
使用Is和As方法自定义错误测试
errors.Is 函数检查链中每个错误是否与目标值匹配。默认情况下,如果两者相等,则错误与目标值匹配。此外,链中的错误可以通过实现 Is 方法声明它与目标值匹配。
作为一个例子,考虑这个错误的灵感来自 Upspin 错误包,它将错误与模板进行比较,只考虑模板中非零的字段。
type Error struct {
Path string
User string
}
func (e *Error) Is(target error) bool {
t, ok := target.(*Error)
if !ok {
return false
}
return (e.Path == t.Path || t.Path == "") &&
(e.User == t.User || t.User == "")
}
if errors.Is(err, &Error{User: "someuser"}) {
// err's User field is "someuser".
}
errors.As 函数同样在存在的情况下咨询 As 方法。
Errors 和包API
返回错误的包(大多数都是这样)应该描述这些错误的属性,程序员可以依赖这些属性。一个设计良好的包也会避免返回具有不应该依赖的属性的错误。
最简单的规范是说,操作要么成功,要么失败,分别返回一个 nil 或 non-nil 的错误值。在很多情况下,不需要进一步的信息。
如果我们希望函数返回一个可识别的错误条件,比如 “item not found”,我们可能会返回一个包裹着 sentinel 的 error。
var ErrNotFound = errors.New("not found")
// FetchItem returns the named item.
//
// If no item with the name exists, FetchItem returns an error
// wrapping ErrNotFound.
func FetchItem(name string) (*Item, error) {
if itemNotFound(name) {
return nil, fmt.Errorf("%q: %w", name, ErrNotFound)
}
// ...
}
还有其他现有的模式可以提供可以被调用者进行语义检查的错误,例如直接返回一个哨兵值、一个特定的类型或一个可以用谓词函数检查的值。
在所有情况下,都应该注意不要向用户暴露内部细节。正如我们在上面的 “是否封装” 中提到的,当你从另一个包中返回一个错误时,你应该将错误转换为不暴露底层错误的形式,除非你愿意承诺在将来返回那个特定的错误。
f, err := os.Open(filename)
if err != nil {
// The *os.PathError returned by os.Open is an internal detail.
// To avoid exposing it to the caller, repackage it as a new
// error with the same text. We use the %v formatting verb, since
// %w would permit the caller to unwrap the original *os.PathError.
return fmt.Errorf("%v", err)
}
如果函数被定义为返回一个包裹着某个哨兵或类型的 error ,不要直接返回底层错误。
var ErrPermission = errors.New("permission denied")
// DoSomething returns an error wrapping ErrPermission if the user
// does not have permission to do something.
func DoSomething() error {
if !userHasPermission() {
// If we return ErrPermission directly, callers might come
// to depend on the exact error value, writing code like this:
//
// if err := pkg.DoSomething(); err == pkg.ErrPermission { … }
//
// This will cause problems if we want to add additional
// context to the error in the future. To avoid this, we
// return an error wrapping the sentinel so that users must
// always unwrap it:
//
// if err := pkg.DoSomething(); errors.Is(err, pkg.ErrPermission) { ... }
return fmt.Errorf("%w", ErrPermission)
}
// ...
}
总结
虽然我们所讨论的变化仅仅是三个函数和一个格式化动词,但我们希望它们将大大改善Go程序中的错误处理方式。我们希望通过包装来提供额外的上下文会变得很普遍,帮助程序做出更好的决策,帮助程序员更快地找到错误。
正如Russ Cox在GopherCon 2019的主题演讲中所说,在通往Go 2的道路上,我们进行实验、简化和出货。现在我们已经出货了这些变化,我们期待着接下来的实验。
5 - panic
5.1 - panic概述
go by example
https://gobyexample-cn.github.io/panic
panic
意味着有些出乎意料的错误发生。 通常我们用它来表示程序正常运行中不应该出现的错误, 或者我们不准备优雅处理的错误。
我们将使用 panic 来检查这个站点上预期之外的错误。 而该站点上只有一个程序:触发 panic。
panic 的一种常见用法是:当函数返回我们不知道如何处理(或不想处理)的错误值时,中止操作。 如果创建新文件时遇到意外错误该如何处理?这里有一个很好的 panic
示例。
package main
import "os"
func main() {
panic("a problem")
_, err := os.Create("/tmp/file")
if err != nil {
panic(err)
}
}
运行程序将会导致 panic: 输出一个错误消息和协程追踪信息,并以非零的状态退出程序:
$ go run panic.go
panic: a problem
goroutine 1 [running]:
main.main()
/.../panic.go:12 +0x47
...
exit status 2
注意,与某些使用 exception 处理错误的语言不同, 在 Go 中,通常会尽可能的使用返回值来标示错误。
Effective Go
panic
https://golang.org/doc/effective_go.html#panic
向调用者报告错误的通常方法是返回 error 作为一个额外的返回值。规范的 Read 方法是一个著名的实例;它返回一个字节数和一个错误。但如果错误无法恢复怎么办?有时,程序根本无法继续。
为此,有一个内置的函数panic,它实际上会产生一个运行时错误,使程序停止(但请看下一节)。这个函数接收一个任意类型的参数–通常是一个字符串–在程序死亡时打印出来。它也是一种指示不可能发生的事情的方法,比如退出一个无限循环。
// A toy implementation of cube root using Newton's method.
func CubeRoot(x float64) float64 {
z := x/3 // Arbitrary initial value
for i := 0; i < 1e6; i++ {
prevz := z
z -= (z*z*z-x) / (3*z*z)
if veryClose(z, prevz) {
return z
}
}
// A million iterations has not converged; something is wrong.
panic(fmt.Sprintf("CubeRoot(%g) did not converge", x))
}
这只是一个例子,但真正的库函数应该避免 panic。如果问题可以被 recover 或解决,让事情继续运行总比把整个程序拆掉要好。一个可能的反例是在初始化过程中:如果库真的无法自我创建,那么可以说,panic 是合理的。
var user = os.Getenv("USER")
func init() {
if user == "" {
panic("no value for $USER")
}
}
Recover
https://golang.org/doc/effective_go.html#recover
当调用 panic 时,包括隐含的运行时错误,如索引分片出界或类型断言失败,它会立即停止执行当前函数,并开始解开(unwind) goroutine 的堆栈,沿途运行任何 defer 函数。如果该解卷(unwind)到达 goroutine 的栈顶,程序就会死亡。然而,可以使用内置函数 recover 来重新获得goroutine的控制权并恢复正常执行。
对 recover 的调用会停止解卷(unwind),并返回传递给 panic 的参数。因为在解卷(unwind)时只有在defer函数内部的代码才能运行,所以 recover 只在defer函数内部有用。
recover的一个应用是在服务器内部关闭一个失败的goroutine,而不杀死其他正在执行的goroutine。
func server(workChan <-chan *Work) {
for work := range workChan {
go safelyDo(work)
}
}
func safelyDo(work *Work) {
defer func() {
if err := recover(); err != nil {
log.Println("work failed:", err)
}
}()
do(work)
}
在这个例子中,如果do(work) panic,结果会被记录下来,goroutine会干净利落地退出,而不会打扰到其他程序。在 defer 闭包中不需要做任何其他事情,调用recover就可以完全处理这个条件。
因为除非直接从 defer 函数中调用 recover,否则 recover 总是返回nil,所以 defer 代码可以调用本身使用panic和recover的库例程而不会失败。举个例子,safeDo中的 defer 函数可能会在调用 recover 之前调用一个日志函数,而这个日志代码的运行不会受到 panic 状态的影响。
有了我们的 recovery 模式,do函数(以及它所调用的任何东西)可以通过调用 panic 来干净利落地摆脱任何糟糕的情况。我们可以用这个想法来简化复杂软件中的错误处理。让我们看看一个理想化版本的 regexp 包,它通过调用 panic 与本地错误类型来报告解析错误。下面是Error的定义、错误方法和Compile函数。
// Error is the type of a parse error; it satisfies the error interface.
type Error string
func (e Error) Error() string {
return string(e)
}
// error is a method of *Regexp that reports parsing errors by
// panicking with an Error.
func (regexp *Regexp) error(err string) {
panic(Error(err))
}
// Compile returns a parsed representation of the regular expression.
func Compile(str string) (regexp *Regexp, err error) {
regexp = new(Regexp)
// doParse will panic if there is a parse error.
defer func() {
if e := recover(); e != nil {
regexp = nil // Clear return value.
err = e.(Error) // Will re-panic if not a parse error.
}
}()
return regexp.doParse(str), nil
}
如果doParse panic,recover 块将把返回值设置为nil- defer 函数可以修改命名的返回值。然后,它将在对err的赋值中,通过断言它具有本地类型Error来检查问题是否是解析错误。如果没有,类型断言将失败,导致运行时错误,继续堆栈展开,就像什么都没有中断一样。这个检查意味着,如果发生了意外的事情,比如索引出界,即使我们使用panic和recover来处理解析错误,代码也会失败。
有了错误处理,错误方法(因为它是一个绑定到类型的方法,所以它的名字和内置的错误类型相同是很好的,甚至是很自然的)就可以很容易地报告解析错误,而不用担心手动解开解析栈。
if pos == 0 {
re.error("'*' illegal at start of expression")
}
虽然这个模式很有用,但它应该只在一个包内使用。Parse将其内部的 panic 调用转化为错误值;它不会将 panic 暴露给客户端。这是一个很好的规则。
顺便说一下,如果实际发生了错误,这个重新 panic 成语会改变panic值。然而,原始的和新的故障都会在崩溃报告中呈现,所以问题的根本原因仍然可见。因此,这种简单的重新panic方法通常已经足够了– 毕竟是崩溃,但如果你想只显示原始值,你可以多写一点代码来过滤意外的问题,并用原始错误重新panic。这就留给读者去练习了。