指针
指针常见的异常有:指针变量未初始化而指针引用里面的属性、空指针引用、空指针判空或长度。
*
与&
*
可以表示指针变量,使用时,作用于指针变量前表示取值。
&
取地址符,引用变量,表示取对应变量的地址,不可作用于函数。
package main
import "fmt"
import "strings"
func main() {
var p1, p2 *string
p3:= "123-678"
p1 = &p3
// p1指针变量,p3值变量
fmt.Printf("origin p3:%+v, convert after p1:%+v\n", p3, p1)
// `&`无论作用于指针变量,还是值变量,得到的都是其对应的地址
fmt.Printf("origin p3:%+v, convert after p1:%+v\n", &p3, &p1)
p1Copy :=getString(p1)
p2Copy := getString(p2)
fmt.Printf("order 1, origin p1:%+v, convert after:%+v\n", *p1, *getString(p1))
// p1Copy是非nil的指针变量,其前可以加`&`取地址符
fmt.Printf("order 2, origin p1:%+v, convert after:%+v\n", *p1, &p1Copy)
// 注意getString为函数,虽然也是指针类型,但是其前面不能加`&`取地址符,否则编译异常`./main.go:xx: cannot take the address of getString(p1)`
//fmt.Printf("order 3, origin p1:%+v, convert after:%+v\n", *p1, &getString(p1))
// 注意,由于p2指针未初始化,这里p2和p2Copy都为nil
fmt.Printf("order 3, origin p2:%+v, convert after:%+v\n", p2, p2Copy)
// 注意,由于p2指针未初始化,这里p2和p2Copy都为nil,而nil类型的指针变量通过`&`取地址符是可以取到为nil的值的
fmt.Printf("order 4, origin p2:%+v, convert after:%+v\n", p2, &p2Copy)
// 注意这里的p2Copy为nil空指针,无地址,故*p2Copy是无法根据地址取到对应地址里面的值的
fmt.Printf("order 5, origin p2:%+v, convert after:%+v\n", p2, *p2Copy)
// 注意这里的p2为nil空指针,无地址,故*p2是无法根据地址取到对应地址里面的值的
fmt.Printf("order 6, origin p2:%+v, convert after:%+v\n", *p2, p2Copy)
}
func getString(p *string) *string {
var res string
if p == nil {
fmt.Printf("pointer:%p, value:%v\n", p, p)
return nil
}
if strings.Contains(*p, "-") {
res = strings.Replace(*p, "-", ";", -1)
return &res
}
return p
}
对应结果:
origin p3:123-678, convert after p1:0xc42000e220
origin p3:0xc42000e220, convert after p1:0xc42000c028
pointer:0x0, value:<nil>
order 1, origin p1:123-678, convert after:123;678
order 2, origin p1:123-678, convert after:0xc42000c028
order 3, origin p2:<nil>, convert after:<nil>
order 4, origin p2:<nil>, convert after:0xc42000c030
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x485257]
goroutine 1 [running]:
main.main()
/home/cg/root/28475472/main.go:17 +0x407
exit status 2
指针使用注意事项
注意事项:
&
是取地址符,顾名思义,&
无论作用于指针变量,还是值变量,得到的都是其对应的地址。&
是取地址符,但无法作用于函数,因为函数是动态类型,在运行时才能确定其对应的地址,在其前加上&
会在编译时就给出异常。*
在变量声明时表明该变量是指针变量,使用时表示取对应指针变量的值,即通过指针变量的地址找到该地址里面存放的值。- nil类型的指针变量,通过
&
取地址符是可以取到对应地址为nil的值。 - nil类型指针的变量,地址为空也即无地址,无法根据地址通过加
*
的方式取到对应地址里面的值,故也无法对nil类型指针变量的长度加以判断,否则会引发panic: runtime error: invalid memory address or nil pointer dereference
。 - 判断指针类型字符串是否为空,不能仅仅判断长度,需要先判断非nil后,才能进一步通过地址找到里面对应的值,进而判断其值是否为空。
指针字符串判空
可以通过如下方式加以判断:
var p *string
// p != nil才会进行逻辑与后面的长度判断,如果未进行p == nil而直接len(*p)取长度,会因为无地址而找不到对应的值,进而引发`panic: runtime error: invalid memory address or nil pointer dereference`空指针引用异常。
if p != nil && len(*p) != 0 {
fmt.Println("p not empty")
}
数组与切片
数组
package main
import "fmt"
func main() {
var a = [5]int{101, 102, 103, 104, 105}
var r [5]int
fmt.Println("original a =", a)
// aa := a[:]
// aCopy := make([]int, len(aa))
// copy(aCopy, aa)
for i, v := range a {
if i == 0 {
a[1] = 1200
a[2] = 1300
}
r[i] = v
}
fmt.Println("after for range loop, r =", r)
fmt.Println("after for range loop, a =", a)
}
结果如下:
original a = [101 102 103 104 105]
after for range loop, r = [101 102 103 104 105]
after for range loop, a = [101 1200 1300 104 105]
数组转切片
将数组转为slice切片后,使用copy进行值拷贝,再进行结果对比。
package main
import "fmt"
func main() {
var a = [5]int{101, 102, 103, 104, 105}
var r [5]int
fmt.Println("original a =", a)
aa := a[:]
aCopy := make([]int, len(aa))
copy(aCopy, aa)
for i, v := range aCopy {
if i == 0 {
a[1] = 1200
a[2] = 1300
}
r[i] = v
}
fmt.Println("after for range loop, r =", r)
fmt.Println("after for range loop, a =", a)
}
结果:
original a = [101 102 103 104 105]
after for range loop, r = [101 102 103 104 105]
after for range loop, a = [101 1200 1300 104 105]
切片拷贝
将原数组换成切片,再看效果
package main
import "fmt"
func main() {
var a = []int{101, 102, 103, 104, 105}
var r []int
r = make([]int, len(a))
aa := a[:]
aCopy := make([]int, len(aa))
copy(aCopy, aa)
fmt.Printf("original r =%v, addr = %p \n", r, r)
fmt.Printf("original a =%v, addr = %p \n", a, a)
fmt.Printf("original aa =%v, addr = %p \n", aa, aa)
fmt.Printf("original aCopy =%v, addr = %p \n", aCopy, aCopy)
for i, v := range a {
if i == 0 {
a[1] = 1200
a[2] = 1300
}
r[i] = v
//a = append(a, i)
//r = append(r, i)
}
fmt.Printf("after for range loop, r =%v, addr = %p \n", r, r)
fmt.Printf("after for range loop, a =%v, addr = %p \n", a, a)
fmt.Printf("after for range loop, aa =%v, addr = %p \n", aa, aa)
fmt.Printf("after for range loop, aCopy =%v, addr = %p \n", aCopy, aCopy)
}
结果如下:
original r =[0 0 0 0 0], addr = 0xc4200c4030
original a =[101 102 103 104 105], addr = 0xc4200c4000
original aa =[101 102 103 104 105], addr = 0xc4200c4000
original aCopy =[101 102 103 104 105], addr = 0xc4200c4060
after for range loop, r =[101 1200 1300 104 105], addr = 0xc4200c4030
after for range loop, a =[101 1200 1300 104 105], addr = 0xc4200c4000
after for range loop, aa =[101 1200 1300 104 105], addr = 0xc4200c4000
after for range loop, aCopy =[101 102 103 104 105], addr = 0xc4200c4060
切片append
使用append,追加元素后
package main
import "fmt"
func main() {
var a = []int{101, 102, 103, 104, 105}
var r []int
r = make([]int, len(a))
aa := a[:]
aCopy := make([]int, len(aa))
copy(aCopy, aa)
fmt.Printf("original r =%v, addr = %p \n", r, r)
fmt.Printf("original a =%v, addr = %p \n", a, a)
fmt.Printf("original aa =%v, addr = %p \n", aa, aa)
fmt.Printf("original aCopy =%v, addr = %p \n", aCopy, aCopy)
for i, v := range a {
if i == 0 {
a[1] = 1200
a[2] = 1300
}
r[i] = v
a = append(a, i)
r = append(r, i)
}
fmt.Printf("after for range loop, r =%v, addr = %p \n", r, r)
fmt.Printf("after for range loop, a =%v, addr = %p \n", a, a)
fmt.Printf("after for range loop, aa =%v, addr = %p \n", aa, aa)
fmt.Printf("after for range loop, aCopy =%v, addr = %p \n", aCopy, aCopy)
}
结果:
original r =[0 0 0 0 0], addr = 0xc4200141b0
original a =[101 102 103 104 105], addr = 0xc420014180
original aa =[101 102 103 104 105], addr = 0xc420014180
original aCopy =[101 102 103 104 105], addr = 0xc4200141e0
after for range loop, r =[101 1200 1300 104 105 0 1 2 3 4], addr = 0xc4200120f0
after for range loop, a =[101 1200 1300 104 105 0 1 2 3 4], addr = 0xc4200120a0
after for range loop, aa =[101 1200 1300 104 105], addr = 0xc420014180
after for range loop, aCopy =[101 102 103 104 105], addr = 0xc4200141e0
map
json反序列化未使用指针类型
将序列化后的字符串,通过json.Unmarshal反序列化为map时,需要使用指针类型的map形式,否则报错json unmarshal failed json: Unmarshal(non-pointer map[string]string)
。
package main
import "fmt"
import "encoding/json"
func main() {
config := "{\"properties\":\"{\\\"property_211\\\":\\\"211\\\"}\"}"
m := make(map[string]string, 0)
if err := json.Unmarshal([]byte(config), &m); err != nil {
fmt.Println("json unmarshal failed", err)
return
}
fmt.Println("config 1", m)
mm := make(map[string]string, 0)
if err := json.Unmarshal([]byte(config), mm); err != nil {
fmt.Println("json unmarshal failed", err)
return
}
fmt.Println("config 2", mm)
}
结果:
config 1 map[properties:{"property_211":"211"}]
json unmarshal failed json: Unmarshal(non-pointer map[string]string)
空字符串反序列化错误
注意:空字符串,反序列化为map会报错 unmarshal failed, unexpected end of JSON input
,可以对空字符串重置为{}
,然后再反序列化为map[string]interface{}类型。如果确定,要反序列化的字符串的key和value都是字符串类型,而没有像数组、对象等类型,可以将其反序列化为map[string]string类型。
package main
import "fmt"
import "encoding/json"
func main() {
fmt.Println("Hello, World!")
tag := `{"tags":"aaa,231111"}`
tag2 := `{"tags":"6_16"}`
tagMap := make(map[string]string)
if err := json.Unmarshal([]byte(tag), &tagMap); err != nil {
fmt.Printf("unmarshal failed, %s\n", err.Error())
return
}
fmt.Printf("unmarshal success, %+v\n", tagMap)
if val, ok := tagMap["tags"]; ok {
fmt.Println("hit, val:", val)
}
tagMap = make(map[string]string)
if err := json.Unmarshal([]byte(tag2), &tagMap); err != nil {
fmt.Printf("unmarshal failed, %s\n", err.Error())
return
}
fmt.Printf("unmarshal success, %+v\n", tagMap)
if val, ok := tagMap["tags"]; ok {
fmt.Println("2: hit tags, val:", val)
}
// 如果tag3为空字符串``,反序列化报错`unmarshal failed, unexpected end of JSON input`
tag3 := `{}`
tagMap = make(map[string]string)
if err := json.Unmarshal([]byte(tag3), &tagMap); err != nil {
fmt.Printf("unmarshal failed, %s\n", err.Error())
return
}
fmt.Printf("unmarshal success, %+v\n", tagMap)
if val, ok := tagMap["tags"]; ok {
fmt.Println("hit, val:", val)
} else {
fmt.Println("not hit")
}
}
结果为:
Hello, World!
unmarshal success, map[tags:aaa,231111]
hit, val: aaa,231111
unmarshal success, map[tags:6_16]
2: hit tags, val: 6_16
unmarshal success, map[]
not hit
channel
channel未初始化
channel阻塞
http
golang中跟网络相关的问题,比如没有设置超时时间,导致接口调用超时或者不通;请求完接口后,忘记关闭body体,导致连接耗尽,服务请求403等问题。
body忘关闭导致TCP连接耗尽
这个 TCP 问题你得懂:Cannot assign requested address
can’t assign requested address 错误解决
关于 Golang 中 http.Response.Body 未读取导致连接复用问题的一点研究
记得正确处理 http.Response.Body
Cannot assign requested address出现的原因及解决方案(转)
golang http.Response.Body的正确处理方式
出现的问题
查看sch项目detail日志
$ zgrep 68c7dd42-70cc-4723-af09-bf8f8defbe07 detail.log.2022-06-06.0.gz
2022-06-06 18:37:18.101 ERROR [sch/manager.(*Manager).dispatch:799] - () execute http request for url http://xx:xxxx/xxx/68c7dd42-70cc-4723-af09-bf8f8defbe07/onlock failed with resp: &{Status:403 Forbidden StatusCode:403 Proto:HTTP/1.1 ProtoMajor:1 ProtoMinor:1 Header:map[Cache-Control:[no-cache, no-store] Content-Length:[167] Content-Type:[application/json; charset=utf-8] Date:[Mon, 06 Jun 2022 10:37:18 GMT]] Body:0xc0051b7540 ContentLength:167 TransferEncoding:[] Close:false Uncompressed:false Trailer:map[] Request:0xc014d1ea00 TLS:<nil>}
2022-06-06 18:37:20.130 ERROR [sch/manager.(*Manager).dispatch:799] - () execute http request for url http://xx:xxxx/xxx/68c7dd42-70cc-4723-af09-bf8f8defbe07/onlock failed with resp: &{Status:403 Forbidden StatusCode:403 Proto:HTTP/1.1 ProtoMajor:1 ProtoMinor:1 Header:map[Cache-Control:[no-cache, no-store] Content-Length:[167] Content-Type:[application/json; charset=utf-8] Date:[Mon, 06 Jun 2022 10:37:20 GMT]] Body:0xc000d7df00 ContentLength:167 TransferEncoding:[] Close:false Uncompressed:false Trailer:map[] Request:0xc014a47400 TLS:<nil>}
2022-06-06 18:37:22.123 ERROR [sch/manager.(*Manager).dispatch:799] - () execute http request for url http://xx:xxxx/xxx/68c7dd42-70cc-4723-af09-bf8f8defbe07/onlock failed with resp: &{Status:403 Forbidden StatusCode:403 Proto:HTTP/1.1 ProtoMajor:1 ProtoMinor:1 Header:map[Cache-Control:[no-cache, no-store] Co^C
$ cat detail.log | grep 68c7dd42-70cc-4723-af09-bf8f8defbe07
2022-06-07 01:22:09.873 ERROR [xxx/manager.(*Manager).dispatch:797] - () Get "http://xx:xxxx/xxx/68c7dd42-70cc-4723-af09-bf8f8defbe07/onlock": dial tcp xx:xxxx: connect: cannot assign requested address
2022-06-07 01:22:10.298 ERROR [xxx/manager.(*Manager).dispatch:797] - () Get "http://xx:xxxx/xxx/68c7dd42-70cc-4723-af09-bf8f8defbe07/onlock": dial tcp xx:xxxx: connect: cannot assign requested address
2022-06-07 01:22:10.526 ERROR [xxx/manager.(*Manager).dispatch:797] - () Get "http://xx:xxxx/xxx/68c7dd42-70cc-4723-af09-bf8f8defbe07/onlock": dial tcp xx:xxxx: connect: cannot assign requested address
2022-06-07 01:22:11.904 ERROR [git.forwardx.ai/a4stack/erms/schedule-manager/manager.(*Manager).dispatch:797] - () Ge^C
可以看到,由于dial tcp xx:xxxx: connect: cannot assign requested address
,而造成请求接口403禁止访问。查阅该问题,是由于socket连接被占用完导致。
查看mysql数据:
$mysql -h127.0.0.1 -uroot
出现cannot assign requested address的原因
产生这个错误的原因是由于 Linux 分配的客户端连接端口用尽,无法建立 socket 连接导致的。
我们都知道,建立一个连接需要四个部分:目标 IP,目标端口,客户端 IP 和客户端端口。其中前三项是不变的,只有客户端端口不断变化。
那么在大量频繁建立连接时,而端口又不是立即释放,默认是 60s,就会出现客户端端口不够用的情况。
这就是这个问题的本质。
接下来使用两个命令来验证一下:
查看连接数:
# netstat -ae | wc -l
# netstat -ae | grep TIME_WAIT | wc -l
查看可用端口范围:
# sysctl -a | grep port_range
net.ipv4.ip_local_port_range = 50000 65000
结果就是连接数是远大于可用端口数的。
解决方式
怎么解决呢?有两个方案:
- 调低 TIME_WAIT 时间
- 调高可用端口范围
调低 TIME_WAIT 时间
编辑内核文件 /etc/sysctl.conf,增加以下内容:
// 表示开启 SYN Cookies。当出现 SYN 等待队列溢出时,启用 cookies 来处理,
// 可防范少量 SYN 攻击,默认为 0,表示关闭;
net.ipv4.tcp_syncookies = 1
// 表示开启重用。允许将 TIME-WAIT sockets 重新用于新的 TCP 连接,默认为 0,表示关闭;
net.ipv4.tcp_tw_reuse = 1
// 表示开启 TCP 连接中 TIME-WAIT sockets 的快速回收,默认为 0,表示关闭。
net.ipv4.tcp_tw_recycle = 1
// 修改系默认的 TIMEOUT 时间,默认为 60s
net.ipv4.tcp_fin_timeout = 30
调高可用端口范围
编辑内核文件 /etc/sysctl.conf,增加以下内容:
// 表示用于向外连接的端口范围。设置为 1024 到 65535。
net.ipv4.ip_local_port_range = 1024 65535
最后,执行 sysctl -p 使参数生效。
问题复盘
代码,处理回调时,忘记defer resp.Body.Close()
以关闭连接body。
req, err = http.NewRequest("GET", request.RequestURL, bytes.NewBuffer([]byte("")))
if err != nil {
fmt.Println(err.Error())
result = "failed"
} else {
resp, err := client.Do(req)
if err != nil || resp.StatusCode != 200 {
if err != nil {
fmt.Println(err.Error())
} else {
fmt.Errorf("request url %s failed with resp: %+v", request.RequestURL, resp)
}
result = "failed"
}
// 忘记关闭body
// resp.Body.Close()
}
在 Golang 的 HTTP 库的源代码中,关于 http.Response.Body 的说明如下:
// Body represents the response body.
//
// The response body is streamed on demand as the Body field
// is read. If the network connection fails or the server
// terminates the response, Body.Read calls return an error.
//
// The http Client and Transport guarantee that Body is always
// non-nil, even on responses without a body or responses with
// a zero-length body. It is the caller's responsibility to
// close Body. The default HTTP client's Transport may not
// reuse HTTP/1.x "keep-alive" TCP connections if the Body is
// not read to completion and closed.
//
// The Body is automatically dechunked if the server replied
// with a "chunked" Transfer-Encoding.
//
// As of Go 1.12, the Body will also implement io.Writer
// on a successful "101 Switching Protocols" response,
// as used by WebSockets and HTTP/2's "h2c" mode.
Body io.ReadCloser
注意到其中的一句话:The default HTTP client’s Transport may not reuse HTTP/1.x “keep-alive” TCP connections if the Body is not read to completion and closed., 它说,如果 Body 未被读完且关闭的话,默认的 HTTP 客户端的传输层是不会复用 HTTP/1.x 的 “Keep-Alive” 连接的。 (在 HTTP/1.0 的时代,“Keep-Alive” 还不是默认行为,如果浏览器和服务器支持,可以在请求头和响应头加上“Connection: Keep-Alive”。 然而在 HTTP/1.1 中,“Keep-Alive” 是默认行为,除非特别声明了不允许(比如 Connection: close)^。)
为什么要连接复用?当然是基于性能的考虑啦。多路复用能不香吗?
日常写代码的方式
而再看我们平常写的代码,似乎很少有人注意到这点(只是发送 HTTP 请求,不需要数据的情况):
// 注意是 HTTP(80) 端口的连接,大量用于内网
resp, err := http.Get(`http://www.example.com`)
if err != nil {
panic(err)
}
defer resp.Body.Close()
如果只是忘记 Body.Close(),大概率 linter 是过不了的。但是却没有人提醒你,Body 该不该被读走。 按照文档所述,如果 Body 未被读完的话,连接是不会被复用的。为什么?我认为非常简单:因为 HTTP 客户端无法知道你是否还会用到 Body。 它可不敢帮你读走(以完成一次 Request/Response)以主动复用连接,毕竟万一网络突然卡了,读取 Body 占用大量时间。 本来 Close 一下是很快的,结果因为想要复用却导致一大堆不能用且没关闭的连接。这在标准库的设计上来说是不可取的。
按照 Go 语言中 http.Request/Response 的实现原理:服务端在读完请求(Request)的头部(Headers),或客户端在读完响应(Response)的头部(Headers),以后就算完成了请求或响应的读取,Body 被包装成 ReadCloser 接口作为流交给程序后面自己去读取(并关闭)。这一点非常好,不会因为请求或响应包含一个非常大的 Body 而占用大量内存。
应该考虑的写法
综上所述,文档中关于连接复用的现象的说法就是必然存在的了。 那么我们是不是总是应该在不需要 Body 的地方执行一句
io.Copy(ioutil.Discard, resp.Body)
以读完 Body 呢?就像 defer resp.Body.Close() 需要总是记得关闭那样。
答案我不确定:如果能确定接口非常快并且响应数据少或没有数据,那么执行一下可以想像并无性能损失。但是万一网络卡住,那么,读取 Body 的时候就会耗时很久了。 所以,按需吧。如果你在编写代码时能考虑到这一点(或者加个注释也行),那也是更严谨的体现。
一个用于验证连接复用的例子
下面是一段测试代码,用于验证读取并关闭resp.Body后连接是否真正复用:
package main
import (
"flag"
"io"
"io/ioutil"
"net/http"
)
func issue(discard bool) {
resp, err := http.Get(`http://www.example.com`)
if err != nil {
panic(err)
}
defer resp.Body.Close()
if discard {
// 读取并关闭resp.Body,tcp连接才真正会被复用,可以并发调用加以验证
io.Copy(ioutil.Discard, resp.Body)
} else {
// 不读取并关闭resp.Body
fmt.Println("not read resp.Body")
}
}
func main() {
var discard bool
flag.BoolVar(&discard, `d`, false, `discard body`)
flag.Parse()
issue(discard)
issue(true) // whatever
}
像下面这样,不读走 Body 调用时:
$ sudo strace -qqfe connect ./discard -d=false 2>&1 | grep ‘(80)’
[pid 23647] connect(6, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr(“93.184.216.34”)}, 16) = -1 EINPROGRESS (Operation now in progress)
[pid 23649] connect(6, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr(“93.184.216.34”)}, 16) = -1 EINPROGRESS (Operation now in progress)
多次执行,均能看到同样的结果:发起了 2 次 connect 建立连接。是不是说明连接没有复用?我想是的。 虽然两次的文件描述符都是 6,那是因为每次都关闭了连接,并且按照 Unix 的特点:文件描述符总是从最小未被使用的开始。
而当读走 Body 调用时:
$ sudo strace -qqfe connect ./discard -d=true 2>&1 | grep ‘(80)’
[pid 23743] connect(6, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr(“93.184.216.34”)}, 16) = -1 EINPROGRESS (Operation now in progress)
均只能看到只有 1 次 connect 的调用。 是不是已经能说明连接确实被复用了?我想是的。