Go类型断言| 青训营笔记

138 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 20 天

基于类型断言区别错误类型

思考在os包中文件操作返回的错误集合。I/O可以因为任何数量的原因失败,但是有三种经常的错误必须进行不同的处理:文件已经存在(对于创建操作),找不到文件(对于读取操作),和权限拒绝。os包中提供了三个帮助函数来对给定的错误值表示的失败进行分类:

package os
​
func IsExist(err error) bool
func IsNotExist(err error) bool
func IsPermission(err error) bool

对这些判断的一个缺乏经验的实现可能会去检查错误消息是否包含了特定的子字符串,

func IsNotExist(err error) bool {
    // NOTE: not robust!
    return strings.Contains(err.Error(), "file does not exist")
}

但是处理I/O错误的逻辑可能一个和另一个平台非常的不同,所以这种方案并不健壮,并且对相同的失败可能会报出各种不同的错误消息。在测试的过程中,通过检查错误消息的子字符串来保证特定的函数以期望的方式失败是非常有用的,但对于线上的代码是不够的。

一个更可靠的方式是使用一个专门的类型来描述结构化的错误。os包中定义了一个PathError类型来描述在文件路径操作中涉及到的失败,像Open或者Delete操作;并且定义了一个叫LinkError的变体来描述涉及到两个文件路径的操作,像Symlink和Rename。这下面是os.PathError:

package os
​
// PathError records an error and the operation and file path that caused it.
type PathError struct {
    Op   string
    Path string
    Err  error
}
​
func (e *PathError) Error() string {
    return e.Op + " " + e.Path + ": " + e.Err.Error()
}

大多数调用方都不知道PathError并且通过调用错误本身的Error方法来统一处理所有的错误。尽管PathError的Error方法简单地把这些字段连接起来生成错误消息,PathError的结构保护了内部的错误组件。调用方需要使用类型断言来检测错误的具体类型以便将一种失败和另一种区分开;具体的类型可以比字符串提供更多的细节。

_, err := os.Open("/no/such/file")
fmt.Println(err) // "open /no/such/file: No such file or directory"
fmt.Printf("%#v\n", err)
// Output:
// &os.PathError{Op:"open", Path:"/no/such/file", Err:0x2}

通过类型断言询问行为

下面这段逻辑和net/http包中web服务器负责写入HTTP头字段(例如:"Content-type:text/html")的部分相似。io.Writer接口类型的变量w代表HTTP响应;写入它的字节最终被发送到某个人的web浏览器上。

func writeHeader(w io.Writer, contentType string) error {
    if _, err := w.Write([]byte("Content-Type: ")); err != nil {
        return err
    }
    if _, err := w.Write([]byte(contentType)); err != nil {
        return err
    }
    // ...
}

因为Write方法需要传入一个byte切片而我们希望写入的值是一个字符串,所以我们需要使用[]byte(...)进行转换。这个转换分配内存并且做一个拷贝,但是这个拷贝在转换后几乎立马就被丢弃掉。让我们假装这是一个web服务器的核心部分并且我们的性能分析表示这个内存分配使服务器的速度变慢。这里我们可以避免掉内存分配么?

这个io.Writer接口告诉我们关于w持有的具体类型的唯一东西:就是可以向它写入字节切片。如果我们回顾net/http包中的内幕,我们知道在这个程序中的w变量持有的动态类型也有一个允许字符串高效写入的WriteString方法;这个方法会避免去分配一个临时的拷贝。(这可能像在黑夜中射击一样,但是许多满足io.Writer接口的重要类型同时也有WriteString方法,包括*bytes.Buffer*os.File*bufio.Writer。)

我们不能对任意io.Writer类型的变量w,假设它也拥有WriteString方法。但是我们可以定义一个只有这个方法的新接口并且使用类型断言来检测是否w的动态类型满足这个新接口。

// writeString writes s to w.
// If w has a WriteString method, it is invoked instead of w.Write.
func writeString(w io.Writer, s string) (n int, err error) {
    type stringWriter interface {
        WriteString(string) (n int, err error)
    }
    if sw, ok := w.(stringWriter); ok {
        return sw.WriteString(s) // avoid a copy
    }
    return w.Write([]byte(s)) // allocate temporary copy
}
​
func writeHeader(w io.Writer, contentType string) error {
    if _, err := writeString(w, "Content-Type: "); err != nil {
        return err
    }
    if _, err := writeString(w, contentType); err != nil {
        return err
    }
    // ...
}

为了避免重复定义,我们将这个检查移入到一个实用工具函数writeString中,但是它太有用了以致于标准库将它作为io.WriteString函数提供。这是向一个io.Writer接口写入字符串的推荐方法。