Go 语言
标准库、反射和 cgo
标准库的读写模型

标准库的读写模型

Go 基于 io.Writer 和 io.Reader 接口构建了标准库的输入输出模型。这两个接口分别定义了读和写的行为。

模型支持通过 io.Writer 将抽象数据类型(原生类型、自定义的结构体类型等)直接写入存储数据或传输数据的实体抽象(文件、网络连接、HTTP 请求、HTTP 响应等)。 同时也支持通过 io.Reader 从存储数据或传输数据的实体抽象中读取数据到抽象数据类型实例中。

读写字节序列

对应文件实体抽象的 os.File 结构体类型实现了 io.Reader 和 io.Writer 接口,可以直接读写字节序列。

func directWriteBytesToFile(path string, data []byte) (int, error) {
    f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
    if err != nil {
        return 0, err
    }
 
    defer func() {
        f.Sync()
        f.Close()
    }()
 
    return f.Write(data)
}
 
func directReadBytesFromFile(path string, data []byte) (int, error) {
    f, err := os.Open(path)
    if err != nil {
        return 0, err
    }
 
    defer f.Close()
 
    for {
        n, err := f.Read(data)
        if err != nil {
            if err == io.EOF {
                return n, nil
            }
        }
        return n, err
    }
}
 
func main() {
    file := "./foo.txt"
    text := "Hello, World!"
    buf := make([]byte, len(text))
 
    n, err := directWriteBytesToFile(file, []byte(text))
    if err != nil {
        log.Fatal(err)
        return
    }
    fmt.Printf("write %d bytes to %s\n", n, file)
 
    n, err = directReadBytesFromFile(file, buf)
    if err != nil {
        log.Fatal(err)
        return
    }
    fmt.Printf("read %d bytes from %s: %s\n", n, file, buf)
}
 
// 输出
// write 13 bytes to ./foo.txt
// read 13 bytes from ./foo.txt: Hello, World!

读写抽象数据类型实例

有三种方法:

  • 利用 fmt 包的 Fprint 和 Fscan 函数
  • 利用 binary 包的 Read 和 Write 函数
  • 利用 gob 包的 Decode 和 Encode 函数

fmt 包的 Fprint 和 Fscan 等函数运作本事是扫描和解析读出的文本字符串,这导致其数据还原能力有限,在数据还原方面,二进制编码有先天优势, 虽然 binary 包实现了抽象数据类型的直接读写,但只支持采用定长的抽象数据类型,限制了应用范围。不过 Go 标准库为我们提供了一种更为通用的选择:gob 包。 gob 包支持对任意抽象数据类型实例的直接读写,唯一约束是自定义结构体类型中的字段至少有一个是导出的。

下面我们来看一下 gob 包的使用:

type Player struct {
    Name   string
    Age    int
    Gender string
}
 
func directWriteADTToFile(path string, players []Player) error {
    f, err := os.Create(path)
    if err != nil {
        return err
    }
 
    defer func() {
        f.Sync()
        f.Close()
    }()
 
    enc := gob.NewEncoder(f)
 
    for _, p := range players {
        if err := enc.Encode(p); err != nil {
            return err
        }
    }
 
    return nil
}
 
func main() {
    var players = []Player{
        {"Tom", 21, "male"},
        {"Lucy", 18, "female"},
        {"Lily", 19"female"},
    }
 
    err := directWriteADTToFile("players.gob", players)
    if err != nil {
        log.Fatal(err)
        return
    }
    fmt.Println("write players to players.gob")
 
    f, err := os.Open("players.gob")
    if err != nil {
        log.Fatal(err)
        return
    }
 
    defer f.Close()
 
    var player Player
    dec := gob.NewDecoder(f)
    for {
        if err := dec.Decode(&player); err != nil {
            if err == io.EOF {
                return
            }
            if err != nil {
                log.Fatal(err)
                return
            }
        }
        fmt.Printf("%+v\n", player)
    }
}
 
// 输出
// write players to players.gob
// {Tom 21 male}
// {Lucy 18 female}
// {Lily 19 female}

可以看到,gob 包是直接读写抽象数据类型实例方法中最为理想的那个。同时,gob 包也是 Go 标准库提供的一个序列化、反序列化方案。 和 JSON、XML 等序列化、反序列化方案不同,它的 API 直接支持读写实现了 io.Writer 和 io.Reader 接口的实例。

通过包裹类型读写数据

通过包裹函数返回的包裹类型可以实现对输入数据的过滤、装饰、变换等操作,并将结果再次返回给调用者。 Go 标准库的读写模型广泛运用了包裹函数模式,并且给予这种模式实现了有缓存 I/O、数据格式变换等。

通过包裹类型实现带缓存 I/O

带缓存的 I/O 模式通过维护一个中间的缓存来降低数据读写磁盘操作的频率。我们来看一个带缓存的写文件的例子:

func main() {
    file := "./foo.txt"
 
    f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
    if err != nil {
        log.Fatal(err)
        return
    }
    defer func() {
        f.Sync()
        f.Close()
    }()
 
    data := []byte("Hello, World!\n")
 
    // 初始缓冲区大小为 32 字节
    bio := bufio.NewWriter(f, 32)
 
    // 将数据写入缓冲区,文件内容仍为为空
    bio.Write(data)
    bio.Write(data)
    bio.Write(data)
 
    // 将缓冲区中的所有数据写入文件
    bio.Flush()
}

我们再看一个带缓冲的读文件的例子:

func main() {
    file := "./foo.txt"
 
    f, err := os.Open(file)
    if err != nil {
        log.Fatal(err)
        return
    }
    defer f.Close()
 
    // 初始缓冲区大小为 64 字节
    bio := bufio.NewReader(f, 64)
 
    buf := make([]byte, 32)
    for {
        n, err := bio.Read(buf)
        if err != nil {
            if err == io.EOF {
                return
            }
            log.Fatal(err)
            return
        }
        fmt.Printf("read %d bytes: %s\n", n, buf)
    }
}

综合上面两个例子,我们可以看到,标准库通过包裹函数模式轻松实现了带缓存的 I/O。这充分展示了标准库读写模型的优势。