29、未导出的结构体不会被编码
以小写字母开头的结构体将不会被(json、xml、gob等)编码,因此当你编码这些未导出的结构体时,你将会得到零值。
package main
import (
"encoding/json"
"fmt"
)
type MyData struct {
One int
two string
}
func main() {
in := MyData{1, "two"}
fmt.Printf("%#v\n", in) //prints main.MyData{One:1, two:"two"}
encoded, _ := json.Marshal(in)
fmt.Println(string(encoded)) //prints {"One":1}
var out MyData
json.Unmarshal(encoded, &out)
fmt.Printf("%#v\n", out) //prints main.MyData{One:1, two:""}
}
运行结果:
main.MyData{One:1, two:"two"}
{"One":1}
main.MyData{One:1, two:""}
30、有活动的 Goroutines 下的应用退出
应用将不会等待所有的goroutines完成。这对于初学者而言是个很常见的错误。
package main
import (
"fmt"
"time"
)
func main() {
workerCount := 2
for i := 0; i < workerCount; i++ {
go doit(i)
}
time.Sleep(1 * time.Second)
fmt.Println("all done!")
}
func doit(workerId int) {
fmt.Printf("[%v] is running\n", workerId)
time.Sleep(3 * time.Second)
fmt.Printf("[%v] is done\n", workerId)
}
运行结果:
[0] is running
[1] is running
all done!
一个最常见的解决方法是使用“WaitGroup”变量。它将会让主goroutine等待所有的worker goroutine完成。如果你的应用有长时运行的消息处理循环的worker,你也将需要一个方法向这些goroutine发送信号,让它们退出。你可以给各个worker发送一个“kill”消息。另一个选项是关闭一个所有worker都接收的channel。这是一次向所有goroutine发送信号的简单方式。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
done := make(chan struct{})
workerCount := 2
for i := 0; i < workerCount; i++ {
wg.Add(1)
go doit(i, done, wg)
}
close(done)
wg.Wait()
fmt.Println("all done!")
}
func doit(workerId int, done <-chan struct{}, wg sync.WaitGroup) {
fmt.Printf("[%v] is running\n", workerId)
defer wg.Done()
<-done
fmt.Printf("[%v] is done\n", workerId)
}
如果你运行这个应用,你将会看到:
[1] is running
[1] is done
[0] is running
[0] is done
fatal error: all goroutines are asleep - deadlock!
看起来所有的worker在主goroutine退出前都完成了。为什么会出现死锁?worker退出了,它们也执行了wg.Done()。应用应该没问题啊。
死锁发生是因为各个worker都得到了原始的“WaitGroup”变量的一个拷贝。当worker执行wg.Done()时,并没有在主 goroutine上 的“WaitGroup”变量上生效。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
done := make(chan struct{})
wq := make(chan interface{})
workerCount := 2
for i := 0; i < workerCount; i++ {
wg.Add(1)
go doit(i, wq, done, &wg)
}
for i := 0; i < workerCount; i++ {
wq <- i
}
close(done)
wg.Wait()
fmt.Println("all done!")
}
func doit(workerId int, wq <-chan interface{}, done <-chan struct{}, wg *sync.WaitGroup) {
fmt.Printf("[%v] is running\n", workerId)
defer wg.Done()
for {
select {
case m := <-wq:
fmt.Printf("[%v] m => %v\n", workerId, m)
case <-done:
fmt.Printf("[%v] is done\n", workerId)
return
}
}
}
运行结果:
[0] is running
[0] m => 0
[1] is running
[1] is done
[0] m => 1
[0] is done
all done!
现在它会如预期般工作
31、向无缓存的Channel发送消息,只要目标接收者准备好就会立即返回
发送者将不会被阻塞,除非消息正在被接收者处理。根据你运行代码的机器的不同,接收者的goroutine可能会或者不会有足够的时间,在发送者继续执行前处理消息。
package main
import "fmt"
func main() {
ch := make(chan string)
go func() {
for m := range ch {
fmt.Println("processed:", m)
}
}()
ch <- "cmd.1"
ch <- "cmd.2" //won't be processed
}
运行结果:
processed: cmd.1
processed: cmd.2
32、向已关闭的Channel发送会引起Panic
从一个关闭的channel接收是安全的。在接收状态下的ok的返回值将被设置为false,这意味着没有数据被接收。如果你从一个有缓存的channel接收,你将会首先得到缓存的数据,一旦它为空,返回的ok值将变为false。
向关闭的channel中发送数据会引起panic。这个行为有文档说明,但对于新的Go开发者的直觉不同,他们可能希望发送行为与接收行为很像。
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
for i := 0; i < 3; i++ {
go func(idx int) {
ch <- (idx + 1) * 2
}(i)
}
//get the first result
fmt.Println(<-ch)
close(ch) //not ok (you still have other senders)
//do other work
time.Sleep(2 * time.Second)
}
运行错误:
6
panic: send on closed channel
goroutine 6 [running]:
main.main.func1(0xc420070060, 0x1)
根据不同的应用,修复方法也将不同。可能是很小的代码修改,也可能需要修改应用的设计。无论是哪种方法,你都需要确保你的应用不会向关闭的channel中发送数据。
上面那个有bug的例子可以通过使用一个特殊的废弃的channel来向剩余的worker发送不再需要它们的结果的信号来修复。
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
done := make(chan struct{})
for i := 0; i < 3; i++ {
go func(idx int) {
select {
case ch <- (idx + 1) * 2:
fmt.Println(idx, "sent result")
case <-done:
fmt.Println(idx, "exiting")
}
}(i)
}
//get first result
fmt.Println("result:", <-ch)
close(done)
//do other work
time.Sleep(3 * time.Second)
}
运行结果:
2 sent result
result: 6
1 exiting
0 exiting
33、使用"nil" Channels
在一个nil的channel上发送和接收操作会被永久阻塞。这个行为有详细的文档解释,但它对于新的Go开发者而言是个惊喜。
package main
import (
"fmt"
"time"
)
func main() {
var ch chan int
for i := 0; i < 3; i++ {
go func(idx int) {
ch <- (idx + 1) * 2
}(i)
}
//get first result
fmt.Println("result:", <-ch)
//do other work
time.Sleep(2 * time.Second)
}
运行结果:
fatal error: all goroutines are asleep - deadlock!
这个行为可以在select声明中用于动态开启和关闭case代码块的方法。
package main
import "fmt"
import "time"
func main() {
inch := make(chan int)
outch := make(chan int)
go func() {
var in <-chan int = inch
var out chan<- int
var val int
for {
select {
case out <- val:
out = nil
in = inch
case val = <-in:
out = outch
in = nil
}
}
}()
go func() {
for r := range outch {
fmt.Println("result:", r)
}
}()
time.Sleep(0)
inch <- 1
inch <- 2
time.Sleep(3 * time.Second)
}
运行结果:
result: 1
result: 2
34、传值方法的接收者无法修改原有的值
方法的接收者就像常规的函数参数。如果声明为值,那么你的函数/方法得到的是接收者参数的拷贝。这意味着对接收者所做的修改将不会影响原有的值,除非接收者是一个map或者slice变量,而你更新了集合中的元素,或者你更新的域的接收者是指针。
package main
import "fmt"
type data struct {
num int
key *string
items map[string]bool
}
func (this *data) pmethod() {
this.num = 7
}
func (this data) vmethod() {
this.num = 8
*this.key = "v.key"
this.items["vmethod"] = true
}
func main() {
key := "key.1"
d := data{1, &key, make(map[string]bool)}
fmt.Printf("num=%v key=%v items=%v\n", d.num, *d.key, d.items)
//prints num=1 key=key.1 items=map[]
d.pmethod()
fmt.Printf("num=%v key=%v items=%v\n", d.num, *d.key, d.items)
//prints num=7 key=key.1 items=map[]
d.vmethod()
fmt.Printf("num=%v key=%v items=%v\n", d.num, *d.key, d.items)
//prints num=7 key=v.key items=map[vmethod:true]
}
运行结果:
num=1 key=key.1 items=map[]
num=7 key=key.1 items=map[]
num=7 key=v.key items=map[vmethod:true]
35、关闭HTTP的响应
当你使用标准http库发起请求时,你得到一个http的响应变量。如果你不读取响应主体,你依旧需要关闭它。注意对于空的响应你也一定要这么做。对于新的Go开发者而言,这个很容易就会忘掉。
一些新的Go开发者确实尝试关闭响应主体,但他们在错误的地方做
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func main() {
resp, err := http.Get("http://www.oldboygo.cn/")
// resp, err := http.Get("http://www.baidu.cn/")
defer resp.Body.Close() //not ok
if err != nil {
fmt.Println(err)
return
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
这段代码对于成功的请求没问题,但如果http的请求失败,resp变量可能会是nil,这将导致一个runtime panic。
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x40 pc=0x11eb322]
最常见的关闭响应主体的方法是在http响应的错误检查后调用defer。
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func main() {
// resp, err := http.Get("https://api.ipify.org?format=json")
resp, err := http.Get("https://www.oldboygo.com")
if err != nil {
fmt.Println(err)
return
}
defer resp.Body.Close() //ok, most of the time :-)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
运行结果:
Get https://www.oldboygo.com: dial tcp: lookup www.oldboygo.com: no such host
大多数情况下,当你的http响应失败时,resp变量将为nil,而err变量将是non-nil。然而,当你得到一个重定向的错误时,两个变量都将是non-nil。这意味着你最后依然会内存泄露。
通过在http响应错误处理中添加一个关闭non-nil响应主体的的调用来修复这个问题。另一个方法是使用一个defer调用来关闭所有失败和成功的请求的响应主体。
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func main() {
resp, err := http.Get("https://api.ipify.org?format=json")
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
fmt.Println(err)
return
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
resp.Body.Close()的原始实现也会读取并丢弃剩余的响应主体数据。这确保了http的链接在keepalive http连接行为开启的情况下,可以被另一个请求复用。最新的http客户端的行为是不同的。现在读取并丢弃剩余的响应数据是你的职责。如果你不这么做,http的连接可能会关闭,而无法被重用。
如果http连接的重用对你的应用很重要,你可能需要在响应处理逻辑的后面添加像下面的代码:
_, err = io.Copy(ioutil.Discard, resp.Body)
如果你不立即读取整个响应将是必要的,这可能在你处理json API响应时会发生:
json.NewDecoder(resp.Body).Decode(&data)