Go语言常用标准库
Table of Contents
汇总了几个 Go 语言常用的标准库用法笔记,方便学习查阅。
常用标准库
fmt
- 格式化输入输出,类似于 C 语言中的printf
和scanf
。net/http
- 用于构建 HTTP 客户端和服务器。学习如何处理 HTTP 请求和响应。net/rpc
- 提供了通过网络或其他 I/O 连接对一个对象的导出方法的访问。os
- 提供与操作系统交互的功能,比如文件和目录的操作。io
和io/ioutil
- 用于处理输入输出操作,读取和写入文件。encoding/json
- 用于处理 JSON 数据的编码和解码。time
- 处理时间和日期的功能,包括时间格式化和时间计算。strings
- 提供字符串处理的常用函数。strconv
- 字符串与基本数据类型之间的转换。testing
- 提供对 Go 包的自动化测试的支持。math
- 提供基本的数学常数和函数。math/bits
- Go 1.19 新增,用于 bit 操作。sync
- 提供同步原语,如互斥锁和等待组,适用于并发编程。context
- 用于处理请求的上下文,特别是在处理并发和超时操作时非常重要。errors
- 用于创建和处理错误。log
- 提供简单的日志记录功能。path/filepath
- 操作文件路径的工具,适用于跨平台的路径处理。flag
- 用于解析命令行参数,定义和处理命令行标志(flags)。slices
和maps
- Go 1.21 新增,用于切片和集合的操作。
Standard library - Go Packages
fmt
Printing
通用:
%v 值的默认格式表示
%+v 类似%v,但输出结构体时会添加字段名
%#v 值的Go语法表示
%T 值的类型的Go语法表示
%% 百分号
布尔值:
%t 单词true或false
整数:
%b 表示为二进制
%c 该值对应的unicode码值
%d 表示为十进制
%o 表示为八进制
%q 该值对应的单引号括起来的go语法字符字面值,必要时会采用安全的转义表示
%x 表示为十六进制,使用a-f
%X 表示为十六进制,使用A-F
%U 表示为Unicode格式:U+1234,等价于"U+%04X"
浮点数与复数:
%b 无小数部分、二进制指数的科学计数法,如-123456p-78;参见strconv.FormatFloat
%e 科学计数法,如-1234.456e+78
%E 科学计数法,如-1234.456E+78
%f 有小数部分但无指数部分,如123.456
%F 等价于%f
%g 根据实际情况采用%e或%f格式(以获得更简洁、准确的输出)
%G 根据实际情况采用%E或%F格式(以获得更简洁、准确的输出)
字符串和字节串:
%s 直接输出字符串或者[]byte
%q 该值对应的双引号括起来的go语法字符串字面值,必要时会采用安全的转义表示
%x 每个字节用两字符十六进制数表示(使用a-f)
%X 每个字节用两字符十六进制数表示(使用A-F)
对字符串采用
%x
或%X
时,会给各打印的字节之间加空格。
指针:
%p 表示为十六进制,并加上前导的0x
宽度与精度:
%f: 默认宽度,默认精度
%9f 宽度9,默认精度
%.2f 默认宽度,精度2
%9.2f 宽度9,精度2
%9.f 宽度9,精度0
宽度和精度格式化控制的是Unicode 码值的数量,而不是像 C 语言中的字节数量。因此即使 Go 语言字符串中的中文占据 3 个字节,也被当做一个Unicode 码值。
对于字符串,精度是输出字符数目的最大数量,如果必要会截断字符串。
对于整数,宽度和精度都设置输出总长度。采用精度时表示右对齐并用 0 填充,而宽度默认表示用空格填充。
对于浮点数,宽度设置输出总长度;精度设置小数部分长度(如果有的话),除了%g 和%G,此时精度设置总的数字个数。例如,对数字 123.45
,格式 %6.2f
输出 123.45
;格式 %.4g
输出 123.5
。%e
和 %f
的默认精度是 6
,%g
的默认精度是可以将该值区分出来需要的最小数字个数。
对复数,宽度和精度会分别用于实部和虚部,结果用小括号包裹。因此 %f
用于 1.2+3.4i
输出 (1.200000+3.400000i)
。
其他 flag:
'+' 总是输出数值的正负号;对%q(%+q)会生成全部是ASCII字符的输出(通过转义);
' ' 对数值,正数前加空格而负数前加负号;
'-' 在输出右边填充空白而不是默认的左边(即从默认的右对齐切换为左对齐);
'#' 切换格式
'0' 使用0而不是空格填充,对于数值类型会把填充的0放在正负号后面;
对于 #
,切换格式。
%#o 八进制数前加0
%#x 十六进制数前加0x
%#X 十六进制数前加0X
%#p 指针去掉前面的0x
%#q 如果strconv.CanBackquote返回'真'会输出反引号括起来的未转义字符串(%q)
%#U 输出Unicode格式后,如字符可打印,还会输出空格和单引号括起来的go字面值(%U)
Go 语言格式化字符串会忽略不支持的 flag
如果操作数是一个接口值,那么会使用接口内部保管的值,而不是接口,因此:
var i interface{} = 23
fmt.Printf("%v\n", i)
显式指定参数索引,通过 []
包括数字表示第几个索引。如:
fmt.Sprintf("%[2]d %[1]d\n", 11, 22)
// 22 11
fmt.Sprintf("%[3]*.[2]*[1]f", 12.0, 2, 6)
// 等价于
fmt.Sprintf("%6.2f", 12.0)
// 12.00
可以通过重设索引用于多次打印同一个值:
fmt.Sprintf("%d %d %#[1]x %#x", 16, 17)
// 16 17 0x10 0x11
Scanning
- Scan、Scanf 和 Scanln 从标准输入 os. Stdin 读取文本;
- Fscan、Fscanf、Fscanln 从指定的 io. Reader 接口读取文本;
- Sscan、Sscanf、Sscanln 从一个参数字符串读取文本。
Scanln、Fscanln、Sscanln 会在读取到换行时停止,并要求一次提供一行所有条目;
Scanf、Fscanf、Sscanf 只有在格式化文本末端有换行时会读取到换行为止;
其他函数会将换行视为空白。
Scanf、Fscanf、Sscanf 会根据格式字符串解析参数,类似 Printf。例如 %x
会读取一个十六进制的整数,%v
会按对应值的默认格式读取。
格式规则类似 Printf,有如下区别:
%p 未实现
%T 未实现
%e %E %f %F %g %G 效果相同,用于读取浮点数或复数类型
%s %v 用在字符串时会读取空白分隔的一个片段
flag '#'和'+' 未实现
在无格式化 verb 或 %v
下扫描整数时可以接受常用的进制设置前缀 0
(八进制)和 0x
(十六进制)。
当使用格式字符串进行扫描时,多个连续的空白字符(除了换行符)在输出和输出中都被等价于一个空白符。
在所有的扫描函数里,\r\n
都被视为 \n
。
func Printf(format string, a ...interface{}) (n int, err error)
Printf 根据 format 参数生成格式化的字符串并写入标准输出。返回写入的字节数和遇到的任何错误。
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
Fprintf 根据 format 参数生成格式化的字符串并写入 w。返回写入的字节数和遇到的任何错误。
func Sprintf(format string, a ...interface{}) string
Sprintf 根据 format 参数生成格式化的字符串并返回该字符串。
func Print(a ...interface{}) (n int, err error)
Print 采用默认格式将其参数格式化并写入标准输出。如果两个相邻的参数都不是字符串,会在它们的输出之间添加空格。返回写入的字节数和遇到的任何错误。
func Fprint(w io.Writer, a ...interface{}) (n int, err error)
Fprint 采用默认格式将其参数格式化并写入 w。如果两个相邻的参数都不是字符串,会在它们的输出之间添加空格。返回写入的字节数和遇到的任何错误。
func Sprint(a ...interface{}) string
Sprint 采用默认格式将其参数格式化,串联所有输出生成并返回一个字符串。如果两个相邻的参数都不是字符串,会在它们的输出之间添加空格。
func Println(a ...interface{}) (n int, err error)
Println 采用默认格式将其参数格式化并写入标准输出。总是会在相邻参数的输出之间添加空格并在输出结束后添加换行符。返回写入的字节数和遇到的任何错误。
func Fprintln(w io.Writer, a ...interface{}) (n int, err error)
Fprintln 采用默认格式将其参数格式化并写入 w。总是会在相邻参数的输出之间添加空格并在输出结束后添加换行符。返回写入的字节数和遇到的任何错误。
func Sprintln(a ...interface{}) string
Sprintln 采用默认格式将其参数格式化,串联所有输出生成并返回一个字符串。总是会在相邻参数的输出之间添加空格并在输出结束后添加换行符。
func Errorf(format string, a ...interface{}) error
Errorf 根据 format 参数生成格式化字符串并返回一个包含该字符串的错误。
func Scanf(format string, a ...interface{}) (n int, err error)
Scanf 从标准输入扫描文本,根据 format 参数指定的格式将成功读取的空白分隔的值保存进成功传递给本函数的参数。返回成功扫描的条目个数和遇到的任何错误。
func Fscanf(r io.Reader, format string, a ...interface{}) (n int, err error)
Fscanf 从 r 扫描文本,根据 format 参数指定的格式将成功读取的空白分隔的值保存进成功传递给本函数的参数。返回成功扫描的条目个数和遇到的任何错误。
func Sscanf(str string, format string, a ...interface{}) (n int, err error)
Sscanf 从字符串 str 扫描文本,根据 format 参数指定的格式将成功读取的空白分隔的值保存进成功传递给本函数的参数。返回成功扫描的条目个数和遇到的任何错误。
func Scan(a ...interface{}) (n int, err error)
Scan 从标准输入扫描文本,将成功读取的空白分隔的值保存进成功传递给本函数的参数。换行视为空白。返回成功扫描的条目个数和遇到的任何错误。如果读取的条目比提供的参数少,会返回一个错误报告原因。
func Fscan(r io.Reader, a ...interface{}) (n int, err error)
Fscan 从 r 扫描文本,将成功读取的空白分隔的值保存进成功传递给本函数的参数。换行视为空白。返回成功扫描的条目个数和遇到的任何错误。如果读取的条目比提供的参数少,会返回一个错误报告原因。
func Sscan(str string, a ...interface{}) (n int, err error)
Sscan 从字符串 str 扫描文本,将成功读取的空白分隔的值保存进成功传递给本函数的参数。换行视为空白。返回成功扫描的条目个数和遇到的任何错误。如果读取的条目比提供的参数少,会返回一个错误报告原因。
func Scanln(a ...interface{}) (n int, err error)
Scanln 类似 Scan,但会在换行时才停止扫描。最后一个条目后必须有换行或者到达结束位置。
func Fscanln(r io.Reader, a ...interface{}) (n int, err error)
Fscanln 类似 Fscan,但会在换行时才停止扫描。最后一个条目后必须有换行或者到达结束位置。
func Sscanln(str string, a ...interface{}) (n int, err error)
Sscanln 类似 Sscan,但会在换行时才停止扫描。最后一个条目后必须有换行或者到达结束位置。
net/http
http 包提供了 HTTP 客户端和服务端的实现。
Get、Head、Post 和 PostForm 函数发出 HTTP/HTTPS 请求。
resp, err := http.Get("http://example.com/")
// ...
resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf)
// ...
resp, err := http.PostForm("http://example.com/form", url.Values{"key": {"Value"}, "id": {"123"}})
程序在使用完回复后必须关闭回复的主体。
resp, err := http.Get("http://example.com/")
if err != nil {
// handle error
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
// ...
要管理 HTTP 客户端的头域、重定向策略和其他设置,创建一个 Client:
client := &http.Client{
CheckRedirect: redirectPolicyFunc,
}
resp, err := client.Get("http://example.com")
// ...
req, err := http.NewRequest("GET", "http://example.com", nil)
// ...
req.Header.Add("If-None-Match", `W/"wyzzy"`)
resp, err := client.Do(req)
// ...
要管理代理、TLS 配置、keep-alive、压缩和其他设置,创建一个 Transport:
tr := &http.Transport{
TLSClientConfig: &tls.Config{RootCAs: pool},
DisableCompression: true,
}
client := &http.Client{Transport: tr}
resp, err := client.Get("https://example.com")
Client 和 Transport 类型都可以安全的被多个 go 程同时使用。出于效率考虑,应该一次建立、尽量重用。
ListenAndServe 使用指定的监听地址和处理器启动一个 HTTP 服务端。处理器参数通常是 nil,这表示采用包变量 DefaultServeMux 作为处理器。Handle 和 HandleFunc 函数可以向 DefaultServeMux 添加处理器。
http.Handle("/foo", fooHandler)
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
log.Fatal(http.ListenAndServe(":8080", nil))
要管理服务端的行为,可以创建一个自定义的 Server:
s := &http.Server{
Addr: ":8080",
Handler: myHandler,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
log.Fatal(s.ListenAndServe())
os
os 包提供了操作系统函数的不依赖平台的接口。设计为 Unix 风格的,虽然错误处理是 go 风格的;失败的调用会返回错误值而非错误码。通常错误值里包含更多信息。例如,如果某个使用一个文件名的调用(如 Open、Stat)失败了,打印错误时会包含该文件名,错误类型将为 *PathError
,其内部可以解包获得更多信息。
os 包的接口规定为在所有操作系统中都是一致的。非公用的属性可以从操作系统特定的 syscall 包获取。
下面是一个简单的例子,打开一个文件并从中读取一些数据:
file, err := os.Open ("file. go") // For read access.
if err != nil {
log.Fatal (err)
}
如果打开失败,错误字符串是自解释的,例如:
open file. go: no such file or directory
文件的信息可以读取进一个 []byte
切片。Read 和 Write 方法从切片参数获取其内的字节数。
data := make ([]byte, 100)
count, err := file.Read (data)
if err != nil {
log.Fatal (err)
}
fmt.Printf ("read %d bytes: %q\n", count, data[: count])
如果读取的文件特别大,以上代码只能一次读取 128 字节,因此需要循环读取。
for {
data := make ([]byte, 100)
count, err := file.Read (data)
if err == io. EOF { // End Of File
fmt.Println (data[: count])
}
if err != nil {
log.Fatal (err)
}
fmt.Printf ("read %d bytes: %q\n", count, data[: count])
}
用 append 函数就可以将每次循环读出的字节添加到一个数组中去。
func (f *File) Read (b []byte) (n int, err error)
Read 方法从 f 中读取最多 len (b) 字节数据并写入 b。它返回读取的字节数和可能遇到的任何错误。文件终止标志是读取 0 个字节且返回值 err 为 io. EOF
。
Go 1.16 版本后,新增 os. ReadFile
,与 ioutil. ReadFile
功能类似。
func ReadFile (name string)([]byte, error)
如果需要写入文件,则最好通过 os. OpenFile
通过指定模式和权限的方式去打开文件。
func OpenFile (name string, flag int, perm FileMode) (file *File, err error)
OpenFile 是一个更一般性的文件打开函数,大多数调用者都应用 Open 或 Create 代替本函数。它会使用指定的选项(如 O_RDONLY 等)、指定的模式(如 0666 等)打开指定名称的文件。如果操作成功,返回的文件对象可用于 I/O。如果出错,错误底层类型是 *PathError
。
需要指定多个文件模式,可以使用 |
来连接。
func (f *File) Write (b []byte) (n int, err error)
Write 向文件中写入 len (b) 字节数据。它返回写入的字节数和可能遇到的任何错误。如果返回值n!=len (b)
,本方法会返回一个非 nil 的错误。
func (f *File) WriteString (s string) (ret int, err error)
WriteString 类似 Write,但接受一个字符串参数。
io
io 包提供了对 I/O 原语的基本接口。本包的基本任务是包装这些原语已有的实现(如 os 包里的原语),使之成为共享的公共接口,这些公共接口抽象出了泛用的函数并附加了一些相关的原语的操作。
因为这些接口和原语是对底层实现完全不同的低水平操作的包装,除非得到其它方面的通知,客户端不应假设它们是并发执行安全的。
func ReadAll (r io. Reader) ([]byte, error)
ReadAll 从 r 读取直到出现错误或 EOF,然后返回读取的数据。成功地调用返回 err == nil
,而不是 err == EOF
。因为 ReadAll 被定义为从 src 读取直到 EOF,所以它不会将 Read 中的 EOF 视为要报告的错误。
func WriteString (w io. Writer, s string) (n int, err error)
配合 os 库打开文件, io 库也可以实现文件的读取写入。
func main () {
file, err := os.Open ("test. txt")
if err != nil {
return
}
defer file.Close ()
data, err := io.ReadAll (file)
if err != nil {
return
}
fmt.Println (string (data))
}
func main () {
file, err := os.OpenFile ("test. txt", os. O_APPEND|os. O_CREATE, 0755)
if err != nil {
return
}
defer file.Close ()
_, err = io.WriteString (file, "io write string\n")
if err != nil {
return
}
}
io/ioutil
ioutil 在 io 包的基础上实现了一些工具函数。
func ReadAll (r io. Reader) ([]byte, error)
ReadAll 从 r 读取数据直到 EOF 或遇到 error,返回读取的数据和遇到的错误。成功的调用返回的 err 为 nil 而非 EOF。因为本函数定义为读取 r 直到 EOF,它不会将读取返回的 EOF 视为应报告的错误。
func ReadFile (filename string) ([]byte, error)
ReadFile 从 filename 指定的文件中读取数据并返回文件的内容。成功的调用返回的 err 为 nil 而非 EOF。因为本函数定义为读取整个文件,它不会将读取返回的 EOF 视为应报告的错误。
func WriteFile (filename string, data []byte, perm os. FileMode) error
函数向 filename 指定的文件中写入数据。如果文件不存在将按给出的权限创建文件,否则在写入数据之前清空文件。
func ReadDir (dirname string) ([]os. FileInfo, error)
返回 dirname 指定的目录的目录信息的有序列表。
func TempDir (dir, prefix string) (name string, err error)
在 dir 目录里创建一个新的、使用 prfix 作为前缀的临时文件夹,并返回文件夹的路径。如果 dir 是空字符串,TempDir 使用默认用于临时文件的目录(参见 os. TempDir 函数)。不同程序同时调用该函数会创建不同的临时目录,调用本函数的程序有责任在不需要临时文件夹时摧毁它。
func TempFile (dir, prefix string) (f *os. File, err error)
在 dir 目录下创建一个新的、使用 prefix 为前缀的临时文件,以读写模式打开该文件并返回 os. File 指针。如果 dir 是空字符串,TempFile 使用默认用于临时文件的目录(参见 os. TempDir 函数)。不同程序同时调用该函数会创建不同的临时文件,调用本函数的程序有责任在不需要临时文件时摧毁它。
bufio
bufio 包实现了有缓冲的 I/O。它包装一个 io. Reader 或 io. Writer 接口对象,创建另一个也实现了该接口,且同时还提供了缓冲和一些文本 I/O 的帮助函数的对象。
func (b *Reader) Read (p []byte) (n int, err error)
Read 读取数据写入 p。本方法返回写入 p 的字节数。本方法一次调用最多会调用下层 Reader 接口一次 Read 方法,因此返回值 n 可能小于 len (p)。读取到达结尾时,返回值 n 将为 0 而 err 将为 io. EOF。
func (b *Reader) ReadByte () (c byte, err error)
ReadByte 读取并返回一个字节。如果没有可用的数据,会返回错误。
func (b *Reader) ReadBytes (delim byte) (line []byte, err error)
ReadBytes 读取直到第一次遇到 delim 字节,返回一个包含已读取的数据和 delim 字节的切片。如果 ReadBytes 方法在读取到 delim 之前遇到了错误,它会返回在错误之前读取的数据以及该错误(一般是 io. EOF)。当且仅当 ReadBytes 方法返回的切片不以 delim 结尾时,会返回一个非 nil 的错误。
func (b *Reader) ReadString (delim byte) (line string, err error)
ReadString 读取直到第一次遇到 delim 字节,返回一个包含已读取的数据和 delim 字节的字符串。如果 ReadString 方法在读取到 delim 之前遇到了错误,它会返回在错误之前读取的数据以及该错误(一般是 io. EOF)。当且仅当 ReadString 方法返回的切片不以 delim 结尾时,会返回一个非 nil 的错误。
func (b *Reader) ReadLine () (line []byte, isPrefix bool, err error)
ReadLine 是一个低水平的行数据读取原语。大多数调用者应使用 ReadBytes ('\n')
或 ReadString ('\n')
代替,或者使用 Scanner。
ReadLine 尝试返回一行数据,不包括行尾标志的字节。如果行太长超过了缓冲,返回值 isPrefix 会被设为 true,并返回行的前面一部分。该行剩下的部分将在之后的调用中返回。返回值 isPrefix 会在返回该行最后一个片段时才设为 false。返回切片是缓冲的子切片,只在下一次读取操作之前有效。ReadLine 要么返回一个非 nil 的 line,要么返回一个非 nil 的 err,两个返回值至少一个非 nil。
返回的文本不包含行尾的标志字节("\r\n"或"\n")。如果输入流结束时没有行尾标志字节,方法不会出错,也不会指出这一情况。在调用 ReadLine 之后调用 UnreadByte 会总是吐出最后一个读取的字节(很可能是该行的行尾标志字节),即使该字节不是 ReadLine 返回值的一部分。
func (b *Writer) Write (p []byte) (nn int, err error)
Write 将 p 的内容写入缓冲。返回写入的字节数。如果返回值 nn < len (p),还会返回一个错误说明原因。
func (b *Writer) WriteString (s string) (int, error)
WriteString 写入一个字符串。返回写入的字节数。如果返回值 nn < len (s),还会返回一个错误说明原因。
func (b *Writer) WriteByte (c byte) error
WriteByte 写入单个字节。
func (b *Writer) Flush () error
Flush 方法将缓冲中的数据写入下层的 io. Writer 接口。
encoding/json
将 Go 数据结构编码为 JSON 字符串:
type Person struct {
Name string `json: "name"`
Age int `json: "age"`
}
func main () {
person := Person{Name: "John", Age: 30}
jsonData, _ := json.Marshal (person)
fmt.Println (string (jsonData))
}
将 JSON 字符串解码为 Go 数据结构:
type Person struct {
Name string `json: "name"`
Age int `json: "age"`
}
func main () {
jsonData := `{"name": "John","age": 30}`
var person Person
json.Unmarshal ([]byte (jsonData), &person)
fmt.Println (person)
}
使用json: "-"
忽略字段的编码和解码:
type Person struct {
Name string `json: "name"`
Age int `json: "age"`
Password string `json: "-"`
ExtraInfo string `json: "extra_info, omitempty"`
}
func main () {
person := Person{Name: "John", Age: 30, Password: "secret"}
jsonData, _ := json.Marshal (person)
fmt.Println (string (jsonData))
// 输出:{"name": "John","age": 30,"extra_info": ""}
}
time
time 包提供了时间的显示和测量用的函数。日历的计算采用的是公历。
time. Time
类型表示时间。我们可以通过 time.Now ()
函数获取当前的时间对象,然后获取时间对象的年月日时分秒等信息。
now := time.Now () //获取当前时间
fmt.Printf ("current time:%v\n", now)
year := now.Year () //年
month := now.Month () //月
day := now.Day () //日
hour := now.Hour () //小时
minute := now.Minute () //分钟
second := now.Second () //秒
nanosecond := now.Nanosecond () // 纳秒
weekday := now.Weekday () // 星期
时间戳是自 1970 年 1 月 1 日(08:00:00GMT)至当前时间的总毫秒数。它也被称为 Unix 时间戳(UnixTimestamp)。
now := time.Now () //获取当前时间
timestamp 1 := now.Unix () //时间戳
timestamp 2 := now.UnixNano () //纳秒时间戳
fmt.Printf ("current timestamp 1:%v\n", timestamp 1)
fmt.Printf ("current timestamp 2:%v\n", timestamp 2)
使用 time.Unix () 函数可以将时间戳转为时间格式。
timeObj := time.Unix (timestamp, 0) //将时间戳转为时间格式
fmt.Println (timeObj)
year := timeObj.Year () //年
month := timeObj.Month () //月
day := timeObj.Day () //日
hour := timeObj.Hour () //小时
minute := timeObj.Minute () //分钟
second := timeObj.Second () //秒
fmt.Printf ("%d-%02 d-%02 d %02 d:%02 d:%02 d\n", year, month, day, hour, minute, second)
时间类型有一个自带的方法 Format 进行格式化,需要注意的是 Go 语言中格式化时间模板不是常见的 Y-m-d H:M: S 而是使用 Go 的诞生时间 2006 年 1 月 2 号 15 点 04 分。
如果想格式化为 12 小时方式,需指定 PM。
now := time.Now ()
// 格式化的模板为 Go 的出生时间 2006 年 1 月 2 号 15 点 04 分 Mon Jan
// 24 小时制
fmt.Println (now.Format ("2006-01-02 15:04:05.000 Mon Jan"))
// 12 小时制
fmt.Println (now.Format ("2006-01-02 03:04:05.000 PM Mon Jan"))
fmt.Println (now.Format ("2006/01/02 15:04"))
fmt.Println (now.Format ("15:04 2006/01/02"))
fmt.Println (now.Format ("2006/01/02"))
time.Parse ()
方法可以将一个字符串解析为 time. Time
对象,解析时你需要提供相应的布局字符串来匹配时间格式。
timeStr := "2024-10-01 14:45:02"
parsedTime, err := time. Parse ("2006-01-02 15:04:05", timeStr)
if err != nil {
fmt. Println ("解析时间出错: ", err)
} else {
fmt. Println ("解析后的时间: ", parsedTime)
}
输出结果为解析后的 time. Time 对象:
解析后的时间: 2024-10-11 14:45:02 +0000 UTC
在 Go 语言中,time. Duration
用于表示两个时间点之间的时间间隔。Duration
的单位是纳秒,可以用来表示从纳秒到小时之间的时间段。
通过 time.Since ()
可以计算某个时间点距离当前的时间间隔,返回的是一个 time. Duration
对象。
startTime := time.Now ()
time.Sleep (2 * time. Second) // 模拟耗时操作
elapsed := time.Since (startTime)
fmt.Println ("操作耗时: ", elapsed)
可以通过 Add ()
方法对时间进行加减操作,例如加上或减去一定的时间间隔。
nextWeek := currentTime.Add (7 * 24 * time. Hour)
fmt.Println ("一周后的时间: ", nextWeek)
yesterday := currentTime.Add (-24 * time. Hour)
fmt.Println ("昨天的时间: ", yesterday)
通过 Sub ()
方法,可以计算两个时间点之间的时间差。
startTime := time.Date (2024, 10, 1, 9, 0, 0, 0, time. Local)
duration := currentTime.Sub (startTime)
fmt.Println ("时间差: ", duration)
该方法返回两个 time. Time
对象之间的 Duration
,可以进一步转换为天数、小时数等。
fmt.Println ("时间差(小时): ", duration.Hours ())
fmt.Println ("时间差(分钟): ", duration.Minutes ())
fmt.Println ("时间差(秒): ", duration.Seconds ())
fmt.Println ("时间差(纳秒): ", duration.Nanoseconds ())
time 包提供了两种常用的定时功能:Ticker 和 Timer。
最简单的定时操作是 time.Sleep ()
,它会让当前 goroutine 暂停指定的时间。
fmt.Println ("延时 3 秒执行")
time.Sleep (3 * time. Second)
fmt.Println ("延时结束")
time.After ()
返回一个通道,指定时间之后会向该通道发送当前时间。常用于超时控制。
select {
case <-time.After (2 * time. Second):
fmt.Println ("2 秒后执行")
}
Ticker 是一种定时器,它会按照指定的时间间隔周期性地触发事件。Ticker 通过 NewTicker ()
创建,返回一个通道,通道会定期发送时间信号。
ticker := time.NewTicker (1 * time. Second)
go func () {
for t := range ticker. C {
fmt.Println ("每秒触发一次,当前时间: ", t)
}
}()
time.Sleep (5 * time. Second)
ticker.Stop ()
fmt.Println ("Ticker 已停止")
Timer 是一种一次性触发的定时器,指定时间后触发一次。
timer := time.NewTimer (2 * time. Second)
fmt.Println ("当前时间:", time.Now ())
expirationTime := <-timer. C
fmt.Println ("触发时间:", expirationTime)
time 包支持不同的时区处理,通过 time.LoadLocation ()
可以加载不同的时区,并将时间转换为该时区的时间。
loc, _ := time.LoadLocation ("America/New_York")
nyTime := currentTime.In (loc)
fmt. Println ("纽约时间: ", nyTime)
通过记录操作开始和结束时间,可以很方便地计算代码执行时间。
start := time.Now ()
// 执行一些代码
elapsed := time.Since (start)
fmt.Println ("代码执行时间: ", elapsed)
testing
testing 提供对 Go 包的自动化测试的支持。通过 go test
命令,能够自动执行如下形式的任何函数:
func TestXxx (*testing. T)
其中 Xxx 可以是任何字母数字字符串(但第一个字母需要大写),用于识别测试例程。
在这些函数中,使用 Error, Fail 或相关方法来发出失败信号。
要编写一个新的测试套件,需要创建一个名称以 _test. go
结尾的文件,该文件包含TestXxx
函数,如上所述。将该文件放在与被测试的包相同的包中。该文件将被排除在正常的程序包之外,但在运行 “go test” 命令时将被包含。有关详细信息,请运行 “go help test” 和 “go help testflag” 了解。
如果有需要,可以调用 *T
和 *B
的 Skip 方法,跳过该测试或基准测试:
func TestTimeConsuming (t *testing. T) {
if testing. Short () {
t.Skip ("skipping test in short mode.")
}
...
}
func BenchmarkXxx (*testing. B)
被认为是基准测试,通过 “go test” 命令,加上 -bench flag 来执行。多个基准测试按照顺序运行。
testing flags 的详细描述, 参见 githun源码
func BenchmarkHello (b *testing. B) {
for i := 0; i < b.N; i++ {
fmt. Sprintf ("hello")
}
}
基准函数会运行目标代码 b.N
次。在基准执行期间,会调整 b.N
直到基准测试函数持续足够长的时间。
BenchmarkHello 10000000 282 ns/op
意味着循环执行了 10000000 次,每次循环花费 282 纳秒 (ns)。
如果在运行前基准测试需要一些耗时的配置,则可以先重置定时器:
func BenchmarkBigLen (b *testing. B) {
big := NewBig ()
b.ResetTimer ()
for i := 0; i < b.N; i++ {
big. Len ()
}
}
如果基准测试需要在并行设置中测试性能,则可以使用 RunParallel 辅助函数; 这样的基准测试一般与 go test -cpu 标志一起使用:
func BenchmarkTemplateParallel (b *testing. B) {
templ := template. Must (template. New ("test"). Parse ("Hello, {{.}} !"))
b.RunParallel (func (pb *testing. PB) {
var buf bytes. Buffer
for pb. Next () {
buf. Reset ()
templ. Execute (&buf, "World")
}
})
}
该包还可以运行并验证示例代码。示例函数可以包括以 “Output:” 开头的行注释,并在运行测试时与函数的标准输出进行比较。 (比较时会忽略前导和尾随空格。)这些是一个 example 的例子:
func ExampleHello () {
fmt. Println ("hello")
// Output: hello
}
func ExampleSalutations () {
fmt. Println ("hello, and")
fmt. Println ("goodbye")
// Output:
// hello, and
// goodbye
}
Unordered output:
形式的注释,和 Output:
类似,但是能够以任意顺序匹配行:
func ExamplePerm () {
for _, value := range Perm (4) {
fmt. Println (value)
}
// Unordered output: 4
// 2
// 1
// 3
// 0
}
没有输出注释的示例函数被编译但不执行。
example 声明的命名约定:包,函数 F,类型 T,类型 T 上的方法 M 依次是:
func Example () { ... }
func ExampleF () { ... }
func ExampleT () { ... }
func ExampleT_M () { ... }
可以为包/类型/函数/方法提供多个 example 函数,这通过在名称上附加一个不同的后缀来实现。后缀必须是以小写字母开头。
func Example_suffix () { ... }
func ExampleF_suffix () { ... }
func ExampleT_suffix () { ... }
func ExampleT_M_suffix () { ... }
当一个文件包含一个示例函数,同时至少一个其他函数,类型,变量或常量声明,或没有测试或基准函数时,这个测试文件作为示例存在,通常命名为 example_test. go
T 和 B 的 Run 方法允许定义子单元测试和子基准测试,而不必为每个子测试和子基准定义单独的函数。这使得可以使用 Table-Driven 的基准测试和创建层级测试。它还提供了一种共享通用 setup 和 tear-down 代码的方法:
func TestFoo (t *testing. T) {
// <setup code>
t.Run ("A=1", func (t *testing. T) { ... })
t.Run ("A=2", func (t *testing. T) { ... })
t.Run ("B=1", func (t *testing. T) { ... })
// <tear-down code>
}
每个子测试和子基准测试都有一个唯一的名称:顶级测试的名称和传递给 Run 的名称的组合,以斜杠分隔,并具有用于消歧的可选尾随序列号。
-run 和 -bench 命令行标志的参数是与测试名称相匹配的非固定的正则表达式。对于具有多个斜杠分隔元素(例如子测试)的测试,该参数本身是斜杠分隔的,其中表达式依次匹配每个名称元素。因为它是非固定的,一个空的表达式匹配任何字符串。例如,使用 “匹配” 表示 “其名称包含”:
go test -run '' # Run 所有测试。
go test -run Foo # Run 匹配 "Foo" 的顶层测试,例如 "TestFooBar"。
go test -run Foo/A= # 匹配顶层测试 "Foo",运行其匹配 "A=" 的子测试。
go test -run /A=1 # 运行所有匹配 "A=1" 的子测试。
子测试也可用于控制并行性。所有的子测试完成后,父测试才会完成。在这个例子中,所有的测试是相互并行运行的,当然也只是彼此之间,不包括定义在其他顶层测试的子测试:
func TestGroupedParallel (t *testing. T) {
for _, tc := range tests {
tc := tc // capture range variable
t.Run (tc. Name, func (t *testing. T) {
t.Parallel ()
...
})
}
}
在并行子测试完成之前,Run 方法不会返回,这提供了一种测试后清理的方法:
func TestTeardownParallel (t *testing. T) {
// This Run will not return until the parallel tests finish.
t.Run ("group", func (t *testing. T) {
t.Run ("Test 1", parallelTest 1)
t.Run ("Test 2", parallelTest 2)
t.Run ("Test 3", parallelTest 3)
})
// <tear-down code>
}
测试程序有时需要在测试之前或之后进行额外的设置(setup)或拆卸(teardown)。有时, 测试还需要控制在主线程上运行的代码。为了支持这些和其他一些情况, 如果测试文件包含函数:
func TestMain (m *testing. M)
那么生成的测试将调用 TestMain (m),而不是直接运行测试。TestMain 运行在主 goroutine 中, 可以在调用 m.Run 前后做任何设置和拆卸。应该使用 m.Run 的返回值作为参数调用 os. Exit。在调用 TestMain 时, flag. Parse 并没有被调用。所以,如果 TestMain 依赖于 command-line 标志 (包括 testing 包的标记), 则应该显示的调用 flag. Parse。
一个简单的 TestMain 的实现:
func TestMain (m *testing. M) {
// call flag. Parse () here if TestMain uses flags
// 如果 TestMain 使用了 flags,这里应该加上 flag. Parse ()
os. Exit (m.Run ())
}
sync
sync 包提供了基本的同步基元,如互斥锁。除了 Once 和 WaitGroup 类型,大部分都是适用于低水平程序线程,高水平的同步使用 channel 通信更好一些。
Mutex
sync. Mutex 可能是 sync 包中使用最广泛的原语。它允许在共享资源上互斥访问(不能同时访问):
mutex := &sync. Mutex{}
mutex. Lock ()
// Update 共享变量 (比如切片,结构体指针等)
mutex. Unlock ()
必须指出的是,在第一次被使用后,不能再对 sync. Mutex 进行复制。(sync 包的所有原语都一样)。如果结构体具有同步原语字段,则必须通过指针传递它。
RWMutex
sync. RWMutex 是一个读写互斥锁,它提供了我们上面的刚刚看到的 sync. Mutex 的 Lock 和 UnLock 方法(因为这两个结构都实现了 sync. Locker 接口)。但是,它还允许使用 RLock 和 RUnlock 方法进行并发读取:
mutex := &sync. RWMutex{}
mutex. Lock ()
// Update 共享变量
mutex. Unlock ()
mutex. RLock ()
// Read 共享变量
mutex. RUnlock ()
sync. RWMutex 允许至少一个读锁或一个写锁存在,而 sync. Mutex 允许一个读锁或一个写锁存在。
锁定/解锁sync. RWMutex
读锁的速度比锁定/解锁sync. Mutex
更快,另一方面,在sync. RWMutex
上调用Lock ()
/ Unlock ()
是最慢的操作。
因此,只有在频繁读取和不频繁写入的场景里,才应该使用 sync. RWMutex
。
WaitGroup
sync. WaitGroup 也是一个经常会用到的同步原语,它的使用场景是在一个 goroutine 等待一组 goroutine 执行完成。
sync. WaitGroup 拥有一个内部计数器。当计数器等于 0 时,则 Wait ()
方法会立即返回。否则它将阻塞执行 Wait ()
方法的 goroutine 直到计数器等于 0 时为止。
要增加计数器,我们必须使用 Add (int)
方法。要减少它,我们可以使用 Done ()
(将计数器减 1),也可以传递负数给 Add
方法把计数器减少指定大小,Done ()
方法底层就是通过 Add (-1)
实现的。
在以下示例中,我们将启动 8 个 goroutine,并等待他们完成:
wg := &sync. WaitGroup{}
for i := 0; i < 8; i++ {
wg.Add (1)
go func () {
// Do something
wg.Done ()
}()
}
wg.Wait ()
// 继续往下执行...
每次创建 goroutine 时,我们都会使用 wg.Add (1)
来增加 wg 的内部计数器。我们也可以在 for 循环之前调用 wg.Add (8)
。
与此同时,每个 goroutine 完成时,都会使用 wg.Done ()
减少 wg 的内部计数器。
main goroutine 会在八个 goroutine 都执行 wg.Done ()
将计数器变为 0 后才能继续执行。
Map
sync. Map 是一个并发版本的 Go 语言的 map,我们可以:
- 使用
Store (interface {},interface {})
添加元素。 - 使用
Load (interface {}) interface {}
检索元素。 - 使用
Delete (interface {})
删除元素。 - 使用
LoadOrStore (interface {},interface {}) (interface {},bool)
检索或添加之前不存在的元素。如果键之前在 map 中存在,则返回的布尔值为 true。 - 使用 Range 遍历元素。
m := &sync. Map{}
// 添加元素
m.Store (1, "one")
m.Store (2, "two")
// 获取元素 1
value, contains := m.Load (1)
if contains {
fmt.Printf ("%s\n", value. (string))
}
// 返回已存 value,否则把指定的键值存储到 map 中
value, loaded := m.LoadOrStore (3, "three")
if !loaded {
fmt.Printf ("%s\n", value. (string))
}
m.Delete (3)
// 迭代所有元素
m.Range (func (key, value interface{}) bool {
fmt.Printf ("%d: %s\n", key. (int), value. (string))
return true
})
输出
one
three
1: one
2: two
如你所见,Range 方法接收一个类型为 func (key,value interface {}) bool
的函数参数。如果函数返回了 false,则停止迭代。有趣的事实是,即使我们在恒定时间后返回 false,最坏情况下的时间复杂度仍为 O (n)。
我们应该在什么时候使用 sync. Map 而不是在普通的 map 上使用 sync. Mutex?
当我们对 map 有频繁的读取和不频繁的写入时。当多个 goroutine 读取,写入和覆盖不相交的键时。具体是什么意思呢?
例如,如果我们有一个分片实现,其中包含一组 4 个 goroutine,每个 goroutine 负责 25%的键(每个负责的键不冲突)。在这种情况下,sync. Map 是首选。
Pool
sync. Pool
是一个并发池,负责安全地保存一组对象。它有两个导出方法:
Get () interface{}
用来从并发池中取出元素。Put (interface{})
将一个对象加入并发池。
pool := &sync. Pool{}
pool.Put (NewConnection (1))
pool.Put (NewConnection (2))
pool.Put (NewConnection (3))
connection := pool.Get (). (*Connection)
fmt.Printf ("%d\n", connection. id)
connection = pool.Get (). (*Connection)
fmt.Printf ("%d\n", connection. id)
connection = pool.Get (). (*Connection)
fmt.Printf ("%d\n", connection. id)
输出
1
3
2
需要注意的是Get ()
方法会从并发池中随机取出对象,无法保证以固定的顺序获取并发池中存储的对象。
还可以为sync. Pool
指定一个创建者方法:
pool := &sync. Pool{
New: func () interface{} {
return NewConnection ()
},
}
connection := pool.Get (). (*Connection)
这样每次调用 Get () 时,将返回由在 pool. New 中指定的函数创建的对象(在本例中为指针)。
那么什么时候使用 sync. Pool?有两个用例:
第一个是当我们必须重用共享的和长期存在的对象(例如,数据库连接)时。
第二个是用于优化内存分配。
让我们考虑一个写入缓冲区并将结果持久保存到文件中的函数示例。使用 sync. Pool,我们可以通过在不同的函数调用之间重用同一对象来重用为缓冲区分配的空间。 第一步是检索先前分配的缓冲区(如果是第一个调用,则创建一个缓冲区,但这是抽象的)。然后,defer 操作是将缓冲区放回 sync. Pool 中。
func writeFile (pool *sync. Pool, filename string) error {
buf := pool.Get (). (*bytes. Buffer)
defer pool.Put (buf)
// Reset 缓存区,不然会连接上次调用时保存在缓存区里的字符串 foo
// 编程 foofoo 以此类推
buf.Reset ()
buf.WriteString ("foo")
return ioutil.WriteFile (filename, buf.Bytes (), 0644)
}
Once
sync. Once
是一个简单而强大的原语,可确保一个函数仅执行一次。在下面的示例中,只有一个goroutine
会显示输出消息:
once := &sync. Once{}
for i := 0; i < 4; i++ {
i := i
go func () {
once.Do (func () {
fmt.Printf ("first %d\n", i)
})
}()
}
使用Do (func ())
方法来指定只能被调用一次的部分。
Cond
sync. Cond 可能是 sync 包提供的同步原语中最不常用的一个,它用于发出信号(一对一)或广播信号(一对多)到 goroutine。让我们考虑一个场景,我们必须向一个 goroutine 指示共享切片的第一个元素已更新。创建 sync. Cond 需要 sync. Locker 对象(sync. Mutex 或 sync. RWMutex):
cond := sync.NewCond (&sync. Mutex{})
context
在 Go 语言并发编程中,context 包扮演着至关重要的角色。Golang context 提供了一种在 goroutine 之间传递信号、截止日期和值的机制,简化了并发程序的管理和错误处理。 context 包在 Go 1.7 版本中引入,主要用于并发控制和超时处理。
Context,中文译为"上下文",在 Go 语言中代表 goroutine 的执行环境。 它可以携带截止日期、取消信号以及其他与请求相关的值,用于控制 goroutine 的生命周期和行为。
在并发程序中,经常会遇到以下情况:
- 客户端取消请求: 例如,用户在浏览器中关闭页面或中断下载。
- 超时控制: 防止 goroutine 无限期地阻塞,例如等待网络请求或数据库查询。
- 跨 goroutine 传递信息: 例如,传递请求 ID、用户身份等信息。
如果没有 Context,处理这些情况会变得非常复杂。 Context 提供了一种优雅而统一的方式来管理这些挑战。
context 包提供了几个核心方法:
context.Background ()
: 创建一个空的 Context,作为所有其他 Context 的根节点。context.WithCancel (parent Context)
: 创建一个可取消的 Context。返回一个新的 Context 和一个取消函数 cancel ()。调用 cancel () 函数会向该 Context 及其子 Context 发送取消信号。context.WithDeadline (parent Context, deadline time. Time)
: 创建一个带有截止日期的 Context。当到达截止日期时,Context 会自动取消。context.WithTimeout (parent Context, timeout time. Duration)
: 创建一个带有超时的 Context。当超时时间到达时,Context 会自动取消。context.WithValue (parent Context, key, value interface{})
: 创建一个携带键值对的 Context。用于在 goroutine 之间传递信息。
Context 的使用场景
- 通过 Context 上下文取消 Goroutine
使用 context.WithCancel () 创建可取消的 Context,在需要时调用 cancel () 函数取消 goroutine 的执行。
什么场景下会需要取消 Goroutine?
HTTP 服务器调用数据库并将查询的数据返回给客户端是一个常见业务场景,但如果客户端在中途取消请求会怎样?例如,如果客户端在请求中途关闭浏览器。此时,我们应该立即取消后续的执行处理,以防止我们的系统做不必要的工作。
上下文取消有两个方面:一是监听取消事件,二是发出取消事件。
监听 Context 取消事件。
Context
类型提供了一个 Done ()
方法。每次上下文收到取消事件时,这个方法都会返回一个接收空 struct{}
类型的 channel。
因此,要监听取消事件,我们需要等待 <- ctx.Done ()
。
例如,假设一个 HTTP 服务器需要两秒钟来处理一个事件。如果请求在此之前被取消,我们希望立即返回:
func main () {
// 创建一个监听 8000 端口的 HTTP 服务器
http.ListenAndServe (": 8000", http.HandlerFunc (func (w http. ResponseWriter, r *http. Request) {
ctx := r.Context ()
// 这会打印到 STDOUT 以表明处理已经开始
fmt.Fprint (os. Stdout, "processing request\n")
// 我们使用 select 来处理事件,事件取决于哪个通道先接收到消息
select {
case <-time.After (2 * time. Second):
// 如果我们在 2 秒后收到消息,表示请求已经被处理
w.Write ([]byte ("request processed"))
case <-ctx.Done ():
// 如果请求被取消,将其记录到 STDERR
fmt.Fprint (os. Stderr, "request cancelled\n")
}
}))
}
你可以通过运行服务器并在浏览器上打开 https://localhost:8000 来对此进行测试。如果你在 2 秒之前关闭浏览器,你应该会在终端窗口上看到“request cancelled”字样。
发出 Context 取消事件。
可以通过上下文发出取消事件完成取消操作,这可以通过使用 context 包中的 WithCancel
函数来完成,它返回一个上下文对象和一个函数。
ctx, fn := context.WithCancel (ctx)
返回的这个函数不接受任何参数,也不返回任何内容,当你需要取消上下文时就调用它。
func operation 1 (ctx context. Context) error {
// 假设这个操作由于某种原因失败了返回 error
// 我们使用 time. Sleep 来模拟资源密集型操作
time.Sleep (100 * time. Millisecond)
return errors.New ("failed")
}
func operation 2 (ctx context. Context) {
// 我们使用与前面示例中看到的 HTTP 服务器类似的模式
select {
case <-time.After (500 * time. Millisecond):
fmt.Println ("done")
case <-ctx.Done ():
fmt.Println ("halted operation 2")
}
}
func main () {
// 创建一个新的上下文
ctx := context.Background ()
// 创建一个新的上下文,它的取消函数来自于原来的上下文
ctx, cancel := context.WithCancel (ctx)
// 运行两个操作:一个在不同的 goroutine 中
go func () {
err := operation 1 (ctx)
// 如果此操作返回错误,则取消使用此上下文的所有操作
if err != nil {
cancel ()
}
}()
// operation 2 使用与 operation 1 相同的上下文
operation 2 (ctx)
}
运行代码,输出结果:
halted operation 2
- 设置 Context 上下文超时
使用 context.WithTimeout () 为 HTTP 请求或数据库查询设置超时时间,防止 goroutine 无限期地阻塞。
API 与前面的示例几乎相同,只是增加了一些内容:
// 该上下文将在 3 秒后取消
// 如果需要提前取消,可以像前面一样使用' cancel '函数
ctx, cancel := context.WithTimeout (ctx, 3*time. Second)
// 设置上下文 Deadline 与设置超时类似,只是指定了希望上下文取消的具体时间点,而不是指定时长。
// 此处,上下文将在 2009-11-10 23:00:00被取消
ctx, cancel := context.WithDeadline (ctx, time.Date (2009, time. November, 10, 23, 0, 0, 0, time. UTC))
例如,对外部的 HTTP API 进行调用时,如果耗时太长,最好提前失败并取消请求:
func main () {
// 创建一个新的上下文
// 截止时间为 100 毫秒
ctx := context.Background ()
ctx, cancel := context.WithTimeout (ctx, 100*time. Millisecond)
defer cancel ()
// 发出请求,调用 blog. pi 3. fun 博客主页
req, _ := http.NewRequest (http. MethodGet, "https://blog.pi3.fun", nil)
// 将我们刚刚创建的可取消上下文关联到请求
req = req.WithContext (ctx)
// 创建一个新的 HTTP 客户端并执行请求
client := &http. Client{}
res, err := client.Do (req)
// 如果请求失败,记录到 STDOUT
if err != nil {
fmt.Println ("Request failed: ", err)
return
}
// 请求成功打印状态码
fmt.Println ("Response received, status code: ", res. StatusCode)
}
根据博客主页对你请求的响应速度,你将收到:
Response received, status code: 200
或
Request failed: Get https://blog.axiaoxin.com: context deadline exceeded
- 使用 Context 上下文传值
使用 context.WithValue () 传递请求 ID、用户身份等信息,方便在不同的 goroutine 中访问。
你可以使用上下文变量来传递通用的值。这是比在所有函数调用中将它们作为变量传递的更惯用方法。
例如,考虑一个具有多个函数调用的操作,使用一个公共 ID 来标识它以进行日志记录和监控。
实现这个的最简单的方法是为每个函数调用传递 ID:
func main () {
// 创建一个随机整数作为 ID
rand.Seed (time.Now (). Unix ())
id := rand. Int 63 ()
operation 1 (id)
}
func operation 1 (id int 64) {
// do some work
log.Println ("operation 1 for id: ", id, " completed")
operation 2 (id)
}
func operation 2 (id int 64) {
// do some work
log.Println ("operation 2 for id: ", id, " completed")
}
运行代码,输出:
2009/11/10 23:00:00 operation 1 for id: 767100843235198854 completed
2009/11/10 23:00:00 operation 2 for id: 767100843235198854 completed
为什么 Go 的 rand 包中返回一个 64 位整数时称为
Int 63
?
Int 63
方法从默认 Source 返回一个非负伪随机 63 位整数作为 int 64。
int 64
是 64 位有符号整数类型。它包含 1 个符号位和 63 个有效位。因此任何返回非负
int 64
都会产生 63 位数据(第 64 位,符号位,将始终具有相同的值)。
当你想要传递更多信息时,参数很快就会变得臃肿。
我们可以使用上下文实现相同的功能:
// 我们需要设置一个键来告诉我们数据存储在哪里
const keyID = "id"
func main () {
rand.Seed (time.Now (). Unix ())
ctx := context.WithValue (context.Background (), keyID, rand. Int 63 ())
operation 1 (ctx)
}
func operation 1 (ctx context. Context) {
// do some work
// 我们可以通过传入键从上下文中获取值
log.Println ("operation 1 for id: ", ctx.Value (keyID), " completed")
operation 2 (ctx)
}
func operation 2 (ctx context. Context) {
// do some work
// 相同的 ID 从一个函数调用传递到下一个函数调用
log.Println ("operation 2 for id: ", ctx.Value (keyID), " completed")
}
使用上下文变量传递信息很有用,原因有很多:
- 它是线程安全的:上下文键的值一旦设置就无法修改。为给定键设置另一个值的唯一方法是使用 context. WithValue 创建另一个上下文变量
- 它是传统手艺:context 包在整个 Go 的官方库和应用程序中使用,以传递操作范围的数据。其他开发人员和库通常可以很好地使用这种模式。
注意事项
使用 WithTimeout
或 WithCancel
包装一个_可取消的上下文_将会使代码中的多个位置可以取消上下文,应该避免这种情况。
最佳实践
- Context 应该作为函数的第一个参数传递:这有助于确保 Context 在整个调用链中可用。
- 不要将 Context 存储在结构体中:Context 应该作为参数传递,而不是存储在结构体中。
- 使用 context.Background () 作为根 Context:所有其他 Context 都应该从 context.Background () 派生。
- 不要使用 nil 作为 Context:如果函数需要 Context,则应始终传递一个有效的 Context。
- 使用 context.WithValue () 时要注意键的类型:使用自定义类型作为键可以避免键冲突。
- 避免在
context
中存储过多数据:只存储少量、请求范围的数据,如追踪 ID。 - 优先使用常量作为 key:避免 key 冲突,确保代码的可读性。
- 及时调用
cancel
函数:创建context
后应及时调用cancel
函数以释放资源。 - 传递给涉及外部资源的函数:仅在可能阻塞或长时间运行的操作中使用
context
。
常见问题
在 Golang 中,context
包主要用于管理请求的生命周期,特别是在处理并发任务、控制超时和取消操作时,context
非常重要。下面整理了开发者在使用context
时常见的问题和解答。
- 什么是 Golang 中的
context
,它的主要用途是什么?
回答: Golang 中的context
包提供了一个Context
类型,用于在多个 Go 协程之间传递截止时间、取消信号和请求范围的数据。其主要用途包括:
- 控制请求的生命周期(尤其适合 HTTP 请求的处理)。
- 管理超时、取消操作,避免资源泄露。
- 在 API 之间传递请求范围的元数据,如身份验证信息、追踪 ID 等。
2. context
是如何避免 Goroutine 泄露的?
回答: context
通过在请求结束或超时时自动触发取消信号,从而终止所有使用该上下文的 Goroutine,避免了资源泄露。例如,在数据库查询或 API 请求中设置context
,当请求超时或用户取消时,相关的协程也会随之终止。
- 如何创建一个带超时的
context
?
回答: 可以使用context. WithTimeout
函数来创建一个带超时的上下文。例如:
ctx, cancel := context.WithTimeout (context.Background (), 5*time. Second)
defer cancel ()
// 使用 ctx 来控制请求,5 秒后自动取消
在此例中,ctx
将在 5 秒后自动取消,因此适用于一些耗时较长且需要控制的操作。
4. context.Background ()
和context.TODO ()
有什么区别?
回答:
context.Background ()
:通常作为根context
使用,用于整个应用的初始化和顶层的请求处理。context.TODO ()
:多用于代码的占位符,表示开发者尚未确定具体的上下文或未来将添加适当的上下文。
- 该如何选择
context. WithCancel
、context. WithTimeout
和context. WithDeadline
?
回答:
context. WithCancel
:适用于可以手动取消的操作,通常与 API 或服务请求协作,取消请求会传播到所有子context
。context. WithTimeout
:适合需要在一定时间内完成的任务,会在超时后自动取消。context. WithDeadline
:设置一个具体的截止时间点,当时间到达后自动取消上下文,适合任务有精确截止时间的情况。
- 在并发编程中如何使用
context
传递数据?
回答: context
并不是为传递数据设计的,而是用来控制取消信号和超时。因此,context
应仅用于传递少量、请求范围内的数据(如请求 ID),避免将大量数据放在context
中。可以使用context. WithValue
传递特定的值,例如:
ctx := context.WithValue (context.Background (), "userID", 1234)
应避免频繁使用context. WithValue
传递复杂数据,因为这会降低代码的可读性。
7. context
在多线程环境中是否安全?
回答: 是的,context
是线程安全的。多个 Goroutine 可以安全地共享和传递同一个context
。它的只读特性保证了在并发情况下不会发生竞态条件。
8. context
是否支持嵌套?
回答: 支持。一个context
可以衍生出多个子context
,子context
会继承父context
的取消、超时和截止日期。嵌套结构的设计使得可以在不同的协程中控制上下文的生命周期。例如:
parentCtx := context.Background ()
childCtx, cancel := context.WithCancel (parentCtx)
defer cancel ()
在这个例子中,childCtx
继承了parentCtx
的属性。
- 如何从
context
中提取数据?
回答: 可以使用ctx.Value (key)
提取数据,其中key
可以是任意类型的值。需要注意的是,不建议将context
当作全局变量或全局状态来使用,只应传递少量与请求相关的数据。例如:
userID := ctx.Value ("userID")
如果key
不存在,返回的值将为nil
。
10. context
是否应该传递到每一个函数?
回答: 不一定。对于简单的函数或不涉及外部资源的函数,不需要传递context
。context
更适合传递给涉及外部资源(如数据库、网络请求等)的函数中,以便在需要时可以控制超时或取消操作。
- 如何检测一个
context
是否被取消?
回答: 使用 context.Done ()
通道可以检测是否取消。如下所示:
select {
case <-ctx.Done ():
fmt.Println ("Context cancelled")
default:
fmt.Println ("Context active")
}
当Done ()
通道被关闭时表示context
已取消或超时,Goroutine 可以安全退出。
errors
errors 包实现了创建错误值的函数。
func New (text string) error
使用字符串创建一个错误, 请类比 fmt 包的 Errorf 方法,差不多可以认为是 New (fmt. Sprintf (…))。
Example
err := errors. New ("emit macho dwarf: elf header corrupted")
if err != nil {
fmt. Print (err)
}
Output:
emit macho dwarf: elf header corrupted
Example (Errorf)
const name, id = "bimmler", 17
err := fmt. Errorf ("user %q (id %d) not found", name, id)
if err != nil {
fmt. Print (err)
}
Output:
user "bimmler" (id 17) not found
log
log 包实现了简单的日志服务。本包定义了 Logger 类型,该类型提供了一些格式化输出的方法。本包也提供了一个预定义的“标准”Logger,可以通过辅助函数Print[f|ln]
、Fatal[f|ln]
和Panic[f|ln]
访问,比手工创建一个 Logger 对象更容易使用。Logger 会打印每条日志信息的日期、时间,默认输出到标准错误。Fatal 系列函数会在写入日志信息后调用os.Exit (1)
。Panic 系列函数会在写入日志信息后 panic。
log 包的基本使用方法非常简单,主要通过以下几个函数来实现日志记录:
- log.Print ()
- log.Printf ()
- log.Println ()
- log.Fatal ()
- log.Fatalf ()
- log.Fatalln ()
- log.Panic ()
- log.Panicf ()
- log.Panicln ()
这些函数分别提供了不同的日志记录方式,满足不同的需求。
log.Print ("This is a log message")
log.Printf ("This is a formatted log message: %d", 42)
log.Println ("This is a log message with a newline")
// Fatal functions
log.Fatal ("This is a fatal log message")
log.Fatalf ("This is a formatted fatal log message: %d", 42)
log.Fatalln ("This is a fatal log message with a newline")
// Panic functions
log.Panic ("This is a panic log message")
log.Panicf ("This is a formatted panic log message: %d", 42)
log.Panicln ("This is a panic log message with a newline")
默认情况下,log
包的日志输出位置是标准错误输出(stderr)。我们可以通过log.SetOutput ()
函数将日志输出重定向到其他位置,例如文件或自定义的日志处理器。
file, err := os.OpenFile ("app. log", os. O_CREATE|os. O_WRONLY|os. O_APPEND, 0666)
if err != nil {
log.Fatal (err)
}
defer file.Close ()
log.SetOutput (file)
log.Println ("This is a log message written to a file")
log 包允许我们自定义日志消息的前缀和时间格式,通过 log.SetPrefix () 和 log.SetFlags () 函数实现。常见的时间格式标志包括:
- log. Ldate:日期(2009/01/23)
- log. Ltime:时间(01:23:23)
- log. Lmicroseconds:微秒级时间(01:23:23.123123)
- log. Llongfile:完整文件名和行号
- log. Lshortfile:短文件名和行号
- log. LUTC:使用 UTC 时间
log.SetPrefix ("INFO: ")
log.SetFlags (log. Ldate | log. Ltime | log. Lshortfile)
log.Println ("This is a log message with custom prefix and flags")
输出
INFO: 2024/12/20 21:45:12 main. go:10: This is a log message with custom prefix and flags
除了基本功能外,log
包还支持更高级的用法,如创建自定义 Logger、设置日志级别等。我们可以通过log.New ()
函数创建自定义的 Logger,指定输出位置、前缀和日志格式。
func main () {
file, err := os.OpenFile ("custom. log", os. O_CREATE|os. O_WRONLY|os. O_APPEND, 0666)
if err != nil {
log.Fatal (err)
}
defer file.Close ()
customLogger := log.New (file, "CUSTOM: ", log. Ldate|log. Ltime|log. Lshortfile)
customLogger.Println ("This is a message from the custom logger")
}
cat .\custom. log
CUSTOM: 2024/12/20 21:45:49 main. go:16: This is a message from the custom logger
log
包本身没有内置日志级别管理,虽然我们可以通过封装实现简单的日志级别控制,但是更推荐直接使用 Go 1.21 版本新增的 log/slog 标准库。
path/filepath
filepath 包实现了兼容各操作系统的文件路径的实用操作函数。
func Split (path string) (dir, file string)
Split 函数将路径从最后一个路径分隔符后面位置分隔为两个部分(dir 和 file)并返回。如果路径中没有路径分隔符,函数返回值 dir 会设为空字符串,file 会设为 path。两个返回值满足 path == dir+file。
func Join (elem ... string) string
Join 函数可以将任意数量的路径元素放入一个单一路径里,会根据需要添加路径分隔符。结果是经过简化的,所有的空字符串元素会被忽略。
func Dir (path string) string
Dir 返回路径除去最后一个路径元素的部分,即该路径最后一个元素所在的目录。在使用 Split 去掉最后一个元素后,会简化路径并去掉末尾的斜杠。如果路径是空字符串,会返回".";如果路径由 1 到多个路径分隔符后跟 0 到多个非路径分隔符字符组成,会返回单个路径分隔符;其他任何情况下都不会返回以路径分隔符结尾的路径。
func Base (path string) string
Base 函数返回路径的最后一个元素。在提取元素前会求掉末尾的路径分隔符。如果路径是"",会返回".";如果路径是只有一个斜杆构成,会返回单个路径分隔符。
func Ext (path string) string
Ext 函数返回 path 文件扩展名。返回值是路径最后一个路径元素的最后一个’.‘起始的后缀(包括’.’)。如果该元素没有’.‘会返回空字符串。
func Clean (path string) string
Clean 函数通过单纯的词法操作返回和 path 代表同一地址的最短路径。
它会不断的依次应用如下的规则,直到不能再进行任何处理:
- 将连续的多个路径分隔符替换为单个路径分隔符
- 剔除每一个
.
路径名元素(代表当前目录) - 剔除每一个路径内的
..
路径名元素(代表父目录)和它前面的非..
路径名元素 - 剔除开始一个根路径的
..
路径名元素,即将路径开始处的"/..“替换为”/"(假设路径分隔符是’/’)
返回的路径只有其代表一个根地址时才以路径分隔符结尾,如 Unix 的"/“或 Windows 的 C:\
。
如果处理的结果是空字符串,Clean 会返回”."。参见 http://plan9.bell-labs.com/sys/doc/lexnames.html
func Match (pattern, name string) (matched bool, err error)
匹配路径是否符合模板。
pattern:
{ term }
term:
'*' 匹配 0 或多个非路径分隔符的字符
'?' 匹配 1 个非路径分隔符的字符
'[' [ '^' ] { character-range } ']' 字符组(必须非空)
c 匹配字符 c(c != '*', '?', '\\', '[')
'\\' c 匹配字符 c
character-range:
c 匹配字符 c(c != '\\', '-', ']')
'\\' c 匹配字符 c
lo '-' hi 匹配区间[lo, hi]内的字符
Match 要求匹配整个 name 字符串,而不是它的一部分。只有 pattern 语法错误时,会返回 ErrBadPattern。
Windows 系统中,不能进行转义:\\
被视为路径分隔符。
func Walk (root string, walkFn WalkFunc) error
Walk 函数会遍历 root 指定的目录下的文件树,对每一个该文件树中的目录和文件都会调用 walkFn,包括 root 自身。所有访问文件/目录时遇到的错误都会传递给 walkFn 过滤。文件是按词法顺序遍历的,这让输出更漂亮,但也导致处理非常大的目录时效率会降低。Walk 函数不会遍历文件树中的符号链接(快捷方式)文件包含的路径。
flag
flag 包实现了命令行参数的解析。
要求:
使用 flag. String (), Bool (), Int () 等函数注册 flag,下例声明了一个整数 flag,解析结果保存在*int
指针 ip 里:
import "flag"
var ip = flag. Int ("flagname", 1234, "help message for flagname")
如果你喜欢,也可以将 flag 绑定到一个变量,使用 Var 系列函数:
var flagvar int
func init () {
flag. IntVar (&flagvar, "flagname", 1234, "help message for flagname")
}
或者你可以自定义一个用于 flag 的类型(满足 Value 接口)并将该类型用于 flag 解析,如下:
flag. Var (&flagVal, "name", "help message for flagname")
对这种 flag,默认值就是该变量的初始值。
在所有 flag 都注册之后,调用:
flag. Parse ()
来解析命令行参数写入注册的 flag 里。
解析之后,flag 的值可以直接使用。如果你使用的是 flag 自身,它们是指针;如果你绑定到了某个变量,它们是值。
fmt. Println ("ip has value ", *ip)
fmt. Println ("flagvar has value ", flagvar)
解析后,flag 后面的参数可以从 flag. Args () 里获取或用 flag. Arg (i) 单独获取。这些参数的索引为从 0 到 flag. NArg ()-1。
命令行 flag 语法:
-flag
-flag=x
-flag x // 只有非 bool 类型的 flag 可以
可以使用 1 个或 2 个’-‘号,效果是一样的。最后一种格式不能用于 bool 类型的 flag,因为如果有文件名为 0、false 等时, 如下命令:
cmd -x *
其含义会改变。你必须使用-flag=false 格式来关闭一个 bool 类型 flag。
Flag 解析在第一个非 flag 参数(单个"-“不是 flag 参数)之前停止,或者在终止符”–“之后停止。
整数 flag 接受 1234、0664、0 x 1234 等类型,也可以是负数。bool 类型 flag 可以是:
1, 0, t, f, T, F, true, false, TRUE, FALSE, True, False
时间段 flag 接受任何合法的可提供给 time. ParseDuration 的输入。
默认的命令行 flag 集被包水平的函数控制。FlagSet 类型允许程序员定义独立的 flag 集,例如实现命令行界面下的子命令。FlagSet 的方法和包水平的函数是非常类似的。
例子
func init () {
flag.StringVar (&UserName, "username", "admin", "QREcrypt 用户名")
flag.StringVar (&Password, "password", "admin", "QREcrypt 主密钥")
flag.StringVar (&Port, "port", "51122", "服务运行端口")
flag.Parse ()
}
func main () {
……
}
slices maps
Go 1.18 新增了泛型,Go 1.21 新增了 slices 和 maps 这两个泛型库,其中大多数函数接口都使用了泛型实现。