Go语言错误总结(五)

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 runningall 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 donefatal 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 doneall 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.1processed: 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)}

运行错误:​​​​​​​

6panic: 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 resultresult: 61 exiting0 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: 1result: 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)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值