Skip to content

Go 语言实战 第 8 章 标准库

🏷️ Go 《Go 语言实战》

Go 标准库是一组核心包,用来扩展和增强语言的能力。
这些包为语言增加了大量不同的类型。
开发人员可以直接使用这些类型,而不用再写自己的包或者去下载其他人发布的第三方包。

由于这些包和语言绑在一起发布,它们会得到以下特殊的保证:

  • 每次语言更新,哪怕是小更新,都会带有标准库;
  • 这些标准库会严格遵守向后兼容的承诺;
  • 标准库是 Go 语言开发、构建、发布过程的一部分;
  • 标准库由 Go 的构建者们维护和评审;
  • 每次 Go 语言发布新版本时,标准库都会被测试,并评估性能。

这些保证让标准库变得很特殊,开发人员应该尽量利用这些标准库。
使用标准库里的包可以使管理代码变得更容易,并且保证代码的稳定。
不用担心程序无法兼容不同的 Go 语言版本,也不用管理第三方依赖。

8.1 文档和源代码

书中的文档地址打不开,国内的可以参考这个地址:https://golang.google.cn/pkg/。
这里可以看到每个包中的方法定义、说明及示例。

8.2 记录日志

跟踪日志示例:

TRACE: 2022/05/03 14:53:11.470823 D:/github/goinaction/code/chapter8/listing03/listing03.go:15: message

示例:

go
// This sample program demonstrates how to use the base log package.
package main

import (
    "log"
)

func init() {
    log.SetPrefix("TRACE: ")
    log.SetFlags(log.Ldate | log.Lmicroseconds | log.Llongfile)
}

func main() {
    // Println writes to the standard logger.
    log.Println("message")

    // Fatalln is Println() followed by a call to os.Exit(1).
    log.Fatalln("fatal message")

    // Panicln is Println() followed by a call to panic().
    log.Panicln("panic message")
}

Fatal 系列函数用来写日志消息,然后使用 os.Exit(1) 终止程序。
Panic 系列函数用来写日志消息,然后触发一个 panic 。除非程序使用 recover 函数,否则会导致程序打印调用栈后终止。
Print 系列函数是写日志消息的标准方法。

log 包有一个很方便的地方就是,这些日志记录器是多 goroutine 安全的。

关键字 iota 在常量声明区里有特殊的作用。这个关键字让编译器为每个常量复制相同的表达式,直到声明区结束,或者遇到一个新的赋值语句。
关键字 iota 的另一个功能是,iota 的初始值为 0,之后 iota 的值在每次处理为常量后,都会自增 1。

定制的日志记录器:

go
// This sample program demonstrates how to create customized loggers.
package main

import (
    "io"
    "io/ioutil"
    "log"
    "os"
)

var (
    Trace   *log.Logger // Just about anything
    Info    *log.Logger // Important information
    Warning *log.Logger // Be concerned
    Error   *log.Logger // Critical problem
)

func init() {
    file, err := os.OpenFile("errors.txt",
        os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    if err != nil {
        log.Fatalln("Failed to open error log file:", err)
    }

    Trace = log.New(ioutil.Discard,
        "TRACE: ",
        log.Ldate|log.Ltime|log.Lshortfile)

    Info = log.New(os.Stdout,
        "INFO: ",
        log.Ldate|log.Ltime|log.Lshortfile)

    Warning = log.New(os.Stdout,
        "WARNING: ",
        log.Ldate|log.Ltime|log.Lshortfile)

    Error = log.New(io.MultiWriter(file, os.Stderr),
        "ERROR: ",
        log.Ldate|log.Ltime|log.Lshortfile)
}

func main() {
    Trace.Println("I have something standard to say")
    Info.Println("Special Information")
    Warning.Println("There is something you need to know about")
    Error.Println("Something has failed")
}

当某个等级的日志不重要时,使用 ioutil.Discard 变量可以禁用这个等级的日志。

io.MultiWriter 函数可以实现同时向多个 Writer 写入。

运行结果:

INFO: 2022/05/03 15:16:57 listing11.go:44: Special Information
WARNING: 2022/05/03 15:16:57 listing11.go:45: There is something you need to know about
ERROR: 2022/05/03 15:16:57 listing11.go:46: Something has failed

8.3 编码/解码

标准库里提供了 xmljson 包用来处理对应格式的数据。

这是一个解码 json 响应数据的示例:

go
// This sample program demonstrates how to decode a JSON response
// using the json package and NewDecoder function.
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
)

type (
    // gResult maps to the result document received from the search.
    gResult struct {
        GsearchResultClass string `json:"GsearchResultClass"`
        UnescapedURL       string `json:"unescapedUrl"`
        URL                string `json:"url"`
        VisibleURL         string `json:"visibleUrl"`
        CacheURL           string `json:"cacheUrl"`
        Title              string `json:"title"`
        TitleNoFormatting  string `json:"titleNoFormatting"`
        Content            string `json:"content"`
    }

    // gResponse contains the top level document.
    gResponse struct {
        ResponseData struct {
            Results []gResult `json:"results"`
        } `json:"responseData"`
    }
)

func main() {
    uri := "http://ajax.googleapis.com/ajax/services/search/web?v=1.0&rsz=8&q=golang"

    // Issue the search against Google.
    resp, err := http.Get(uri)
    if err != nil {
        log.Println("ERROR:", err)
        return
    }
    defer resp.Body.Close()

    // Decode the JSON response into our struct type.
    var gr gResponse
    err = json.NewDecoder(resp.Body).Decode(&gr)
    if err != nil {
        log.Println("ERROR:", err)
        return
    }

    fmt.Println(gr)

    // Marshal the struct type into a pretty print
    // version of the JSON document.
    pretty, err := json.MarshalIndent(gr, "", "    ")
    if err != nil {
        log.Println("ERROR:", err)
        return
    }

    fmt.Println(string(pretty))
}

结构字段类型后面的 json:"GsearchResultClass" 叫做 标签(tag ,是提供每个字段的元信息的一种机制,将 JSON 文档和结构类型里的字段一一映射起来。如果不存在标签,对应的结构类型里的字段就包含其零值。

解码 JSON 字符串:

go
// Unmarshal the JSON string into our variable.
var c Contact
err := json.Unmarshal([]byte(JSON), &c)

解码 JSON 字符串到 map 变量:

go
var c map[string]interface{}
err := json.Unmarshal([]byte(JSON), &c)

编码 JSON:

序列化(marshal 是指将数据转换为 JSON 字符串的过程。

下面是将一个 map 序列化为 JSON 字符串的示例:

go
// This sample program demonstrates how to marshal a JSON string.
package main

import (
    "encoding/json"
    "fmt"
    "log"
)

func main() {
    // Create a map of key/value pairs.
    c := make(map[string]interface{})
    c["name"] = "Gopher"
    c["title"] = "programmer"
    c["contact"] = map[string]interface{}{
        "home": "415.333.3333",
        "cell": "415.555.5555",
    }

    // Marshal the map into a JSON string.
    data, err := json.MarshalIndent(c, "", "    ")
    if err != nil {
        log.Println("ERROR:", err)
        return
    }

    fmt.Println(string(data))
}

打印结果:

json
{
    "contact": {
        "cell": "415.555.5555",
        "home": "415.333.3333"
    },
    "name": "Gopher",
    "title": "programmer"
}

使用 json.MarshalIndent 函数将一个 map 值转换为 JSON 字符串。

MarshalIndent函数返回一个 byte 切片。
MarshalIndent函数使用反射来确定如何将 map 类型转换为 JSON 字符串。

8.4 输入和输出

io 包是围绕着实现了 io.Writerio.Reader 接口类型的值而构建的。所有实现了这两个接口的类型的值,都可以使用 io 包提供的所有功能,也可以用其它包里接受这两个接口的函数以及方法。

go
// Sample program to show how different functions from the
// standard library use the io.Writer interface.
package main

import (
    "bytes"
    "fmt"
    "os"
)

// main is the entry point for the application.
func main() {
    // Create a Buffer value and write a string to the buffer.
    // Using the Write method that implements io.Writer.
    var b bytes.Buffer
    b.Write([]byte("Hello "))

    // Use Fprintf to concatenate a string to the Buffer.
    // Passing the address of a bytes.Buffer value for io.Writer.
    fmt.Fprintf(&b, "World!")

    // Write the content of the Buffer to the stdout device.
    // Passing the address of a os.File value for io.Writer.
    b.WriteTo(os.Stdout)
}

使用 io 包实现一个类型 curl 命令的功能。

go
// Sample program to show how to write a simple version of curl using
// the io.Reader and io.Writer interface support.
package main

import (
    "io"
    "log"
    "net/http"
    "os"
)

// main is the entry point for the application.
func main() {
    // r here is a response, and r.Body is an io.Reader.
    r, err := http.Get(os.Args[1])
    if err != nil {
        log.Fatalln(err)
    }

    // Create a file to persist the response.
    file, err := os.Create(os.Args[2])
    if err != nil {
        log.Fatalln(err)
    }
    defer file.Close()

    // Use MultiWriter so we can write to stdout and
    // a file on the same write operation.
    dest := io.MultiWriter(os.Stdout, file)

    // Read the response and write to both destinations.
    io.Copy(dest, r.Body)
    if err := r.Body.Close(); err != nil {
        log.Println(err)
    }
}

8.5 小结

  • 标准库有特殊的保证,并且被社区广泛应用。
  • 使用标准库的包会让你的代码更易于管理,别人也会更信任你的代码。
  • 100 余个包被合理组织,分布在 38 个类别里。
  • 标准库里的 log 包拥有记录日志所需的一切功能。
  • 标准库里的 xmljson 包让处理这两种数据格式变得很简单。
  • io 包支持以流的方式高效处理数据。
  • 接口允许你的代码组合已有的功能。
  • 阅读标准库的代码是熟悉 Go 语言习惯的好方法。