云原生系列Go语言篇-标准库Part 2

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
任务调度 XXL-JOB 版免费试用,400 元额度,开发版规格
可观测可视化 Grafana 版,10个用户账号 1个月
简介: 使用Go进行开发的最大优势之一是其标准库。与Python类似,Go也采取了“内置电池”的理念,提供了构建应用程序所需的许多工具。由于Go是一种相对较新的语言,它附带了一个专注于现代编程环境中遇到的问题的库。

使用Go进行开发的最大优势之一是其标准库。与Python类似,Go也采取了“内置电池”的理念,提供了构建应用程序所需的许多工具。由于Go是一种相对较新的语言,它附带了一个专注于现代编程环境中遇到的问题的库。

我们无法涵盖所有标准库包,所幸也不需要,因为有许多优秀的信息源可以了解标准库,比如官方文档。我们将重点关注几个最重要的包及其设计和用法来演示地道Go语言的基本原则。一些包(errorssynccontexttestingreflectunsafe)在各自的章节中进行过介绍。在本章中,我们将学习Go对I/O、时间、JSON和HTTP的内置支持。

I/O和它的小伙伴们

要使程序有价值,它需要能读取和写出数据。Go的输入/输出理念的核心在io包中有体现。特别是,在该包中定义的两个接口可能是Go中第二和第三最常用的接口:io.Readerio.Writer

注:第一名是谁呢?自然是error,我们已经在错误处理一章中学习过了。

io.Readerio.Writer各自定义了一个方法:

type Reader interface {
    Read(p []byte) (n int, err error)
}
type Writer interface {
    Write(p []byte) (n int, err error)
}

io.Writer接口中的Write方法接收一个字节切片参数,位于接口的实现中。它返回写入的字节数,如果出现错误则返回错误信息。io.Reader中的Read方法更有趣。它不是通过返回参数来返回数据,而是将一个切片作为入参传入实现,并进行修改。最多会将len(p)个字节写入到该切片中。该方法返回写入的字节数。这可能看起来有点奇怪。读者期望的可能是:

type NotHowReaderIsDefined interface {
    Read() (p []byte, err error)
}

标准库中定义io.Reader的方式是有原因的。我们来编写一个函数说明如何使用io.Reader方便大家理解:

func countLetters(r io.Reader) (map[string]int, error) {
    buf := make([]byte, 2048)
    out := map[string]int{}
    for {
        n, err := r.Read(buf)
        for _, b := range buf[:n] {
            if (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z') {
                out[string(b)]++
            }
        }
        if err == io.EOF {
            return out, nil
        }
        if err != nil {
            return nil, err
        }
    }
}

有三点需要注意。首先,我们只需创建一次缓冲区,在每次调用r.Read.时复用它即可。这样我们能够使用单次内存分配读取可能很大的数据源。如果Read方法返回[]byte,那么每次调用都需要重新分配内存。每次分配最终都会出现在堆上,这会给垃圾回收器带来很大的工作量。

如果我们想进一步减少分配,可以在程序启动时创建一个缓冲池。然后在函数开始处从池中获取一个缓冲区,结束时归还。通过将切片传递给io.Reader,内存分配就由开发人员所控制。

其次,我们使用r.Read返回的n值来了解有多少字节被写入缓冲区,并遍历buf切片的子切片,处理所读取的数据。

最后,在r.Read返回的错误是io.EOF时,对r的读取就结束了。这个错误有点奇怪,因为它实际上并不是一个错误。它表示io.Reader中没有剩余可读取的内容。在返回io.EOF时,我们结束处理并返回结果。

io.ReaderRead方法有一个特别之处。在大多数情况下,在函数或方法具有错误返回值时,我们在尝试处理非错误返回值之前先检查错误。但在Read的情况中情况相反,因为在数据流结束或意外情况触发错误之前可能已经返回了一些字节,所以操作相反。

注:如果意外到达了io.Reader的末尾,会返回一个另一个哨兵错误(io.ErrUnexpectedEOF)。注意它以字符串Err开头,表示这是一种意料外的状态。

因为io.Readerio.Writer接口非常简单,可以用多种方式进行实现。我们可以使用strings.NewReade函数通过字符串创建一个io.Reader

s := "The quick brown fox jumped over the lazy dog"
sr := strings.NewReader(s)
counts, err := countLetters(sr)
if err != nil {
    return err
}
fmt.Println(counts)

我们在接口是类型安全的鸭子类型中讨论过,io.Readerio.Writer的实现通常以装饰器模式链接。由于countLetters依赖于io.Reader,我们可以使用完全相同的countLetters函数来计算gzip压缩文件中的英文字母。首先编写一个函数,给定文件名时,返回*gzip.Reader

func buildGZipReader(fileName string) (*gzip.Reader, func(), error) {
    r, err := os.Open(fileName)
    if err != nil {
        return nil, nil, err
    }
    gr, err := gzip.NewReader(r)
    if err != nil {
        return nil, nil, err
    }
    return gr, func() {
        gr.Close()
        r.Close()
    }, nil
}

这个函数演示了实现io.Reader合适的封装类型。我们创建了一个*os.File符合io.Reader接口),在确保其为有效之后,将它传递给gzip.NewReader函数,该函数返回一个*gzip.Reader实例。如果有效,我们返回*gzip.Reader和一个关闭器闭包,当调用它时可以恰如其分地清理我们的资源。

*gzip.Reader实现了io.Reader,我们可以像之前使用的*strings.Reader一样使其与countLetters一起使用:

r, closer, err := buildGZipReader("my_data.txt.gz")
if err != nil {
    return err
}
defer closer()
counts, err := countLetters(r)
if err != nil {
    return err
}
fmt.Println(counts)

因为我们有用于读取和写入的标准接口,在io包中有一个标准函数用于从io.Reader拷贝至io.Writer,即io.Copy。还有其他标准函数可为已有的io.Readerio.Writer实例添加新功能。其中包括:

io.MultiReader返回一个从多个io.Reader实例逐一读取的io.Readerio.LimitReader返回一个仅从提供的io.Reader中读取指定字节数的io.Readerio.MultiWriter返回一个同时向多个io.Writer实例写入的io.Writer

其它标准库的包提供了各自的类型和函数,用于处理io.Readerio.Writer。我们已学习过一些,但还有很多。有压缩算法、存档、加密、缓冲、字节切片和字符串。

io中还定义了其他单个方法的接口,如io.Closerio.Seeker

type Closer interface {
        Close() error
}
type Seeker interface {
        Seek(offset int64, whence int) (int64, error)
}

io.Closer接口由像os.File这样需要在读取或写入完成时进行清理的类型实现。通常,使用defer调用Close函数:

f, err := os.Open(fileName)
if err != nil {
    return nil, err
}
defer f.Close()
// use f

警告: 如果在循环中打开资源,请不要使用defer,因为它在函数退出时才会执行。应该在循环迭代结束之前调用Close方法。如果存在可能导致退出的错误,你也必须在该处调用Close方法。

io.Seeker接口用于对资源进行随机访问。whence参数的有效值为io.SeekStartio.SeekCurrentio.SeekEnd这些常量。本应使用自定义类型来更清晰地表示,但出现了一个令人吃惊的设计失误,whence的类型是int

io包中定义了组合这四个接口各种组合。它们有io.ReadCloserio.ReadSeekerio.ReadWriteCloserio.ReadWriteSeekerio.ReadWriterio.WriteCloserio.WriteSeeker。使用这些接口来指定函数期望对数据的操作。例如,不单使用os.File作为参数,而是使用接口来明确指定函数如何处理参数。这不仅会使函数更通用,还会让开发者的意图更加清晰。此外,如果你正在编写自己的数据源和接收端,要保持代码与这些接口兼容。总体来说,尽量创建像io中定义的接口一样简单和解耦的接口。它们展示了简单抽象的强大。

ioutil包提供了一些简单的实用工具,用于将整个io.Reader实现一次性读入字节切片,读取和写入文件以及处理临时文件等。ioutil.ReadAllioutil.ReadFileioutil.WriteFile函数可处理小型数据源,但对于大数据源最好使用bufio包中的ReaderWriterScanner来做处理。

ioutil中更巧妙的一个函数演示了如何为Go类型添加方法的模式。如果一个类型实现了io.Reader但没有实现io.Closer的类型(比如strings.Reader),并且需要将其传递给接收io.ReadCloser的函数,可以将io.Reader传递给ioutil.NopCloser函数,会得到一个实现了io.ReadCloser的类型。其实现非常简单:

type nopCloser struct {
    io.Reader
}
func (nopCloser) Close() error { return nil }
func NopCloser(r io.Reader) io.ReadCloser {
    return nopCloser{r}
}

在需要为类型添加额外的方法实现接口时,可以使用这种嵌入类型模式。

注:ioutil.NopCloser函数违反了不从函数返回接口的一般规则,但它是一个用于确定不会改变的接口的简单适配器,因为它来自标准库。

time

和大部分编程语言一样,Go标准库包含对时间支持,位于time包中。有两种表示时间的主要类型,time.Durationtime.Time

时间段由time.Duration表示,其类型为int64。Go可以表示的最小时间单位是一纳秒,但time包定义了time.Duration类型的常量来表示纳秒、微秒、毫秒、秒、分钟和小时。例如,可以用以下方式表示2小时30分钟的时长:

d := 2 * time.Hour + 30 * time.Minute // d is of type time.Duration

这些常量使得time.Duration既易读又类型安全。它们展示了对带类型常量很好的使用。

Go 定义了一个易理解的字符串格式,由一系列数字组成,可以用time.ParseDuration函数解析为time.Duration。如标准库文档所述:

时长字符串是有符号的十进制数序列,可带小数及后接单位,例如 "300ms"、"-1.5h" 或 "2h45m"。有效的时间单位包括 "ns"、"us"(或 "µs")、"ms"、"s"、"m"、"h"。

- Go 标准库文档

time.Duration上定义了多个方法。它实现了fmt.Stringer接口,并通过 String 方法返回格式化的时长字符串。它有获取小时、分钟、秒、毫秒、微秒或纳秒等数值的方法。TruncateRound 方法将time.Duration截取或四舍五入为指定的time.Duration单位。

某个时间由time.Time类型表示,包含时区。可以使用 time.Now函数获取当前时间。它返回一个本地时区的time.Time实例。

小贴士:time.Time实例包含时区信息,因此不应使用==来检查两个time.Time实例是否对应同一时刻。而应使用Equal方法,该方法会校正时区。

time.Parse函数将字符串转换为time.Time,而Format方法将time.Time转换为字符串。尽管 Go 通常采用曾经运行良好的想法,但它使用自有的日期和时间格式化语言。将日期和时间格式化为 "2006年1月2日 下午3点04分05秒 MST(山区标准时间)" 来指定格式。

注:为什么选择这个日期?因为其中的每个部分依次代表了数字 1 到 7,即 01/02 03:04:05PM '06 -0700(MST是UTC的7 小时前)。

例如,以下代码:

t, err := time.Parse("2006-02-01 15:04:05 -0700", "2016-13-03 00:00:00 +0000")
if err != nil {
    return err
}
fmt.Println(t.Format("January 2, 2006 at 3:04:05PM MST"))

会打印出:

March 13, 2016 at 12:00:00AM UTC

虽然用于格式化的日期和时间进行了巧妙的辅助记忆的设计,但依然很难记住,每次用的时候都要查阅(注:1.20中已内置了time.DateTime等常量,如time.DateTime表示2006-01-02 15:04:05)。所幸在 time 包中,最常用的日期和时间格式都有自己的常量。

就像在time.Duration上定义了部分提取的方法一样,对time.Time也定义了类似的方法,包括 DayMonthYearHourMinuteSecondWeekdayClock(将time.Time的以单独的小时、分钟和秒int值返回)和Date(将年、月和日以单独的int值返回)。可以使用 AfterBeforeEqual方法比较两个time.Time实例。

Sub方法返回一个time.Duration,表示两个time.Time实例之间经过的时间,而Add方法返回time.Duration时长之后的time.TimeAddDate方法返回一个新的 time.Time实例,该实例按指定的年、月和日增加。与time.Duration一样,它也定义了TruncateRound方法。所有这些方法都是在值接收器上定义的,因此它们不会修改time.Time实例。

单调时间

大多数操作系统会追踪两种不同类型的时间:墙上时钟(wall clock),对应于当前时间,和单调时钟(monotonic clock),它是从计算机启动时开始递增。之所以要跟踪两个不同的时钟是因为墙上时间不是统一递增的。夏令时、闰秒和 NTP(网络时间协议)更新可能会导致墙上时间意外地前后移动。这可能会在设置计时器或计算经过的时长时引发问题。

为了解决这个潜在问题,Go 在设置计时器或使用time.Now创建time.Time实例时使用单调时间来记录经过的时间。这种支持是隐式的,计时器会自动使用它。如果两个time.Time实例都设置了单调时间,Sub方法会使用单调时钟来计算time.Duration。如果它们没有设置单调时间(因为其中一个或两个实例没有使用time.Now创建),Sub方法会使用实例中指定的时间来计算time.Duration

注:如果想了解在未正确处理单调时间时会有什么问题,请参阅Cloudflare博客中详细介绍的早期 Go 版本中由于缺乏单调时间支持而引发的错误的文章

计时器和超时

正如我们在如何让代码超时中介绍的那样,time包中包含了返回在指定时间后输出值的通道的函数。time.After函数返回一个仅输出一次的通道,而由time.Tick返回的通道在指定的time.Duration间隔后每次输出一个新值。这些与 Go 的并发支持一起使用,以实现超时或定期任务。你还可以使用time.AfterFunc函数在指定的时间间隔后触发某个函数的运行。不要在复杂程序中使用time.Tick,因为底层的time.Ticker无法关闭(因此无法进行垃圾回收)。而应使用time.NewTicker函数,它返回一个*time.Ticker,其中包含要监听的通道,以及重置和停止计时器的方法。

相关文章
|
7天前
|
存储 Go
Go 语言入门指南:切片
Golang中的切片(Slice)是基于数组的动态序列,支持变长操作。它由指针、长度和容量三部分组成,底层引用一个连续的数组片段。切片提供灵活的增减元素功能,语法形式为`[]T`,其中T为元素类型。相比固定长度的数组,切片更常用,允许动态调整大小,并且多个切片可以共享同一底层数组。通过内置的`make`函数可创建指定长度和容量的切片。需要注意的是,切片不能直接比较,只能与`nil`比较,且空切片的长度为0。
Go 语言入门指南:切片
|
2天前
|
存储 缓存 监控
企业监控软件中 Go 语言哈希表算法的应用研究与分析
在数字化时代,企业监控软件对企业的稳定运营至关重要。哈希表(散列表)作为高效的数据结构,广泛应用于企业监控中,如设备状态管理、数据分类和缓存机制。Go 语言中的 map 实现了哈希表,能快速处理海量监控数据,确保实时准确反映设备状态,提升系统性能,助力企业实现智能化管理。
20 3
|
1天前
|
运维 监控 算法
监控局域网其他电脑:Go 语言迪杰斯特拉算法的高效应用
在信息化时代,监控局域网成为网络管理与安全防护的关键需求。本文探讨了迪杰斯特拉(Dijkstra)算法在监控局域网中的应用,通过计算最短路径优化数据传输和故障检测。文中提供了使用Go语言实现的代码例程,展示了如何高效地进行网络监控,确保局域网的稳定运行和数据安全。迪杰斯特拉算法能减少传输延迟和带宽消耗,及时发现并处理网络故障,适用于复杂网络环境下的管理和维护。
|
6天前
|
开发框架 前端开发 Go
eino — 基于go语言的大模型应用开发框架(二)
本文介绍了如何使用Eino框架实现一个基本的LLM(大语言模型)应用。Eino中的`ChatModel`接口提供了与不同大模型服务(如OpenAI、Ollama等)交互的统一方式,支持生成完整响应、流式响应和绑定工具等功能。`Generate`方法用于生成完整的模型响应,`Stream`方法以流式方式返回结果,`BindTools`方法为模型绑定工具。此外,还介绍了通过`Option`模式配置模型参数及模板功能,支持基于前端和用户自定义的角色及Prompt。目前主要聚焦于`ChatModel`的`Generate`方法,后续将继续深入学习。
93 6
|
2天前
|
存储 缓存 安全
Go 语言中的 Sync.Map 详解:并发安全的 Map 实现
`sync.Map` 是 Go 语言中用于并发安全操作的 Map 实现,适用于读多写少的场景。它通过两个底层 Map(`read` 和 `dirty`)实现读写分离,提供高效的读性能。主要方法包括 `Store`、`Load`、`Delete` 等。在大量写入时性能可能下降,需谨慎选择使用场景。
|
7天前
|
存储 开发框架 Devops
eino — 基于go语言的大模型应用开发框架(一)
Eino 是一个受开源社区优秀LLM应用开发框架(如LangChain和LlamaIndex)启发的Go语言框架,强调简洁性、可扩展性和可靠性。它提供了易于复用的组件、强大的编排框架、简洁明了的API、最佳实践集合及实用的DevOps工具,支持快速构建和部署LLM应用。Eino不仅兼容多种模型库(如OpenAI、Ollama、Ark),还提供详细的官方文档和活跃的社区支持,便于开发者上手使用。
69 8
|
2天前
|
编译器 Go
揭秘 Go 语言中空结构体的强大用法
Go 语言中的空结构体 `struct{}` 不包含任何字段,不占用内存空间。它在实际编程中有多种典型用法:1) 结合 map 实现集合(set)类型;2) 与 channel 搭配用于信号通知;3) 申请超大容量的 Slice 和 Array 以节省内存;4) 作为接口实现时明确表示不关注值。此外,需要注意的是,空结构体作为字段时可能会因内存对齐原因占用额外空间。建议将空结构体放在外层结构体的第一个字段以优化内存使用。
|
7天前
|
存储 算法 Go
Go语言实战:错误处理和panic_recover之自定义错误类型
本文深入探讨了Go语言中的错误处理和panic/recover机制,涵盖错误处理的基本概念、自定义错误类型的定义、panic和recover的工作原理及应用场景。通过具体代码示例介绍了如何定义自定义错误类型、检查和处理错误值,并使用panic和recover处理运行时错误。文章还讨论了错误处理在实际开发中的应用,如网络编程、文件操作和并发编程,并推荐了一些学习资源。最后展望了未来Go语言在错误处理方面的优化方向。
|
3天前
|
SQL 安全 Java
阿里双十一背后的Go语言实践:百万QPS网关的设计与实现
解析阿里核心网关如何利用Go协程池、RingBuffer、零拷贝技术支撑亿级流量。 重点分享: ① 如何用gRPC拦截器实现熔断限流; ② Sync.Map在高并发读写中的取舍。
|
4天前
|
存储 算法 安全
基于 Go 语言的公司内网管理软件哈希表算法深度解析与研究
在数字化办公中,公司内网管理软件通过哈希表算法保障信息安全与高效管理。哈希表基于键值对存储和查找,如用户登录验证、设备信息管理和文件权限控制等场景,Go语言实现的哈希表能快速验证用户信息,提升管理效率,确保网络稳定运行。
14 0