标准库的坑
time 的坑
Go 标准库中的 time 包提供了时间、日期、定时器等日常开发中最常用的时间相关的工具,但很多人在初次使用 time 包时都会遇到下面的示例的问题:
func main() {
t := time.Now()
fmt.Println(t.Format("%Y-%m-%d %H:%M:%S"))
}
// 输出
// %Y-%m-%d %H:%M:%S
很多语言采用字符化的占位符如(%Y %m
等) 拼接处时间的目标输出格式布局如(%Y-%m-%d %H:%M:%S
)几乎是时间格式化输出的标准方案。
但在 Go 语言中采用的是参考时间(reference time)方案。使用参考时间构筑出的时间格式串与最终输出串是一模一样的,这样就省去了开发者再次在大脑中对格式串进行解析的过程:
func main() {
t := time.Now()
fmt.Println(t.Format("2006-01-02 15:04:05"))
}
// 输出
// 2023-10-10 15:04:05
encoding/json 的坑
Go 语言在 Web 服务开发及 API 领域获得开发者的广泛青睐,这使得 encoding/json 包成为了 Go 标准库中使用频率最高的包之一。
1、未导出的结构体字段不会被编码到 JSON 文本中
使用 json 包将结构体类型编码为 JSON 文本十分简单,通过为结构体字段添加表情(tag)的方式来指示其在 JSON 文本中的名字:
type User struct {
Name string `json:"name"`
Age int `json:"age"`
id int `json:"id"`
}
func main() {
u := User{"Tom", 20, 123123}
b, _ := json.Marshal(u)
fmt.Println(string(b))
}
// 输出
// struct field id has json tag but is not exported
// {"name":"Tom","age":20}
我们看到最终输出的 JSON 文本中并没有包含 id 字段。这是因为 json 包默认仅对结构体中的导出字段(字段名首字母大写)进行编码,非导出字段并不会被编码。 解码时亦是如此:
s := `{"name":"Tom","age":20,"id":123123}`
var u User
json.Unmarshal([]byte(s), &u)
fmt.Println(u)
// 输出
// {Tom 20 0}
除了 JSON 外,在 Go 标准库的 encoding 目录下的各类编解码包(如 xml、gob 等)也都遵循相同的规则。
2、nil 切片和空切片可能被编码为不同文本
在日常开发中,我们很容易哦混淆 nil 切片和空切片。 nil 切片是指尚未初始化的切片,Go 运行时尚未为其分配存储空间; 而空切片则是已经初始化了的切片,Go 运行时为其分配了存储空间,但是该切片的长度为 0。
var nilSlice []int
var emptySlice = make([]int, 0, 5)
Println(nilSlice == nil) // true
Println(emptySlice == nil) // false
JSON 包在编码时会区别对待这两种切片:
m := map[string][]int{
"nil": nilSlice,
"empty": emptySlice,
}
b, _ := json.Marshal(m)
fmt.Println(string(b))
// 输出
// {"empty":[],"nil":null}
我们看到空切片被编码为[]
,而 nil 切片被编码为 null。
3、字节切片可能被编码为 base64 文本
一般情况下,字符串与字节切片的区别在于前者存储的是合法 Unicode 字符的 utf-8 编码,而字节切片中可以存储任意字节序列。因此 json 包在编码时会区别对待这两种类型数据:
func main() {
m := map[string]interface{}{
"bytes": []byte{"hello, go"},
"str": "hello, go",
}
b, _ := json.Marshal(m)
fmt.Println(string(b))
}
// 输出
// {"bytes":"aGVsbG8sIGdv","str":"hello, go"}
我们看到字节切片被编码为 base64 文本,而字符串则被编码为普通文本。
我们可以使用 echo "aGVsbG8sIGdv" | base64 -d
,还原出文本内容。
如果你的字节切片中存储的仅是合法的 utf-8 编码的字符串,那么你可以将其转换为字符串类型,然后再进行编码。
4、当 JSON 文本中的整型数值被解码为 interface 类型时,其底层真实类型为 float64
很多时候 JSON 文本中的字段不确定,我们常用 map[string]interface{}
来存储 JSON 包解码后的数据,
这样 JSON 字段值就会存储在一个 interface{}
变量中,通过类型断言来获取其中存储的整型值,改造后的例子如下:
type User struct {
Name string
Age int
}
func main() {
s := `{"name":"Tom","age":20}`
var u map[string]interface{}
json.Unmarshal([]byte(s), &u)
fmt.Println(u["age"].(int))
}
// 输出
// panic: interface conversion: interface {} is float64, not int
json 包提供了 Number 类型来存储 JSON 文本中的各类数值类型,并可以转换为整型(int64)、浮点型(float64)及字符串。
结合 json.Decoder
来修正上面的问题:
func main() {
s := `{"name":"Tom","age":20}`
m := map[string]interface{}{}
d := json.NewDecoder(strings.NewReader(s))
d.UseNumber()
d.Decode(&m)
age, _ := m["age"].(json.Number).Int64()
fmt.Println(age)
}
// 输出
// 20
net/http 的坑
http 包也是整个标准库使用频率最高的包之一。
1、http 包需要我们手动关闭 Response.Body
通过 http 包我们很容易实现一个 HTTP 客户端,如下:
func main() {
resp, err := http.Get("https://linpx.com")
if err != nil {
fmt.Println(err)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
仅在作为客户端时,http 包才需要我们手动关闭 Response.Body
。而在作为服务端时,http 包会自动处理 Response.Body
。
2、HTTP 客户端默认不会及时关闭已经用完的 HTTP 连接
Go 的标准库 HTTP 客户端的默认实现并不会及时关闭已经用完的 HTTP 连接(仅当服务端主动关闭或要求关闭时才会关闭),这样一旦连接建立过多又得不到及时释放, 就很可能会出现端口资源或文件描述符资源耗尽的异常。
及时释放 HTTP 的连接有两种方法。
第一种是将 http.Request 中的字段 Close 设置为 true。
func main() {
req, _ := http.NewRequest("GET", "https://linpx.com", nil)
req.Close = true
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
}
该例子没有直接使用 http.Get
函数,而是使用 http.NewRequest
函数创建了一个 http.Request
对象,然后将其 Close 字段设置为 true,最后通过 http.DefaultClient.Do
方法发送请求。
当收到并读取完应答后,http 包就会及时关闭该连接。
第二种方法是通过创建一个 http.Client
新实例来实现。
func main() {
client := &http.Client{
Transport: &http.Transport{
DisableKeepAlives: true,
},
}
resp, _ := client.Get("https://linpx.com")
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
}
在创建 http.Client
实例时,我们通过 http.Transport
的 DisableKeepAlives
字段来关闭 HTTP 连接的复用,即设置了与服务端不保持长连接,
这样使用该 Client 实例与服务端收发数据后及时关闭两种之间的 HTTP 连接。