老男孩教育专注IT教育10余年,只培养IT技术精英

全国免费咨询电话(渠道合作):400-609-2893

Go语言错误总结(七)!老男孩Golang培训班

老男孩IT教育

技术博客

2022年4月18日 16:28

本篇文章为大家介绍一下Go语言错误总结(七),快来学习吧!

  43、类型声明和方法

  当你通过把一个现有(非interface)的类型定义为一个新的类型时,新的类型不会继承现有类型的方法。

  错误代码:

package main

import "sync"

type myMutex sync.Mutex

func main() {
    var mtx myMutex
    mtx.Lock() 
    mtx.Unlock() 
}

  编译错误:

./main.go:9:5: mtx.Lock undefined (type myMutex has no field or method Lock)
./main.go:10:5: mtx.Unlock undefined (type myMutex has no field or method Unlock)

  如果你确实需要原有类型的方法,你可以定义一个新的struct类型,用匿名方式把原有类型嵌入其中。

  正确代码:

package main

import "sync"

type myLocker struct {
    sync.Mutex
}

func main() {
    var lock myLocker
    lock.Lock()
    lock.Unlock()
}

  interface类型的声明也会保留它们的方法集合。

package main

import "sync"

type myLocker sync.Locker

func main() {
    var lock myLocker = new(sync.Mutex)
    lock.Lock()
    lock.Unlock()
}

  44、从"for switch"和"for select"代码块中跳出

  没有标签的“break”声明只能从内部的switch/select代码块中跳出来。如果无法使用“return”声明的话,那就为外部循环定义一个标签是另一个好的选择。

package main

import "fmt"

func main() {
loop:
    for {
        switch {
        case true:
            fmt.Println("breaking out...")
            break loop
        }
    }
    fmt.Println("out!")
}

  运行结果:

breaking out...
out!

  "goto"声明也可以完成这个功能。。。

  45、"for"声明中的迭代变量和闭包

  这在Go中是个很常见的技巧。for语句中的迭代变量在每次迭代时被重新使用。这就意味着你在for循环中创建的闭包(即函数字面量)将会引用同一个变量(而在那些goroutine开始执行时就会得到那个变量的值)。

package main

import (
    "fmt"
    "time"
)

func main() {
    data := []string{"one", "two", "three"}
    for _, v := range data {
        go func() {
            fmt.Println(v)
        }()
    }
    time.Sleep(3 * time.Second)
    //goroutines print: three, three, three
}

  运行结果:

three
three
three

  最简单的解决方法(不需要修改goroutine)是,在for循环代码块内把当前迭代的变量值保存到一个局部变量中。

package main

import (
    "fmt"
    "time"
)

func main() {
    data := []string{"one", "two", "three"}
    for _, v := range data {
        vcopy := v //
        go func() {
            fmt.Println(vcopy)
        }()
    }
    time.Sleep(3 * time.Second)
    //goroutines print: one, two, three
}

  运行结果:

three
one
two

  另一个解决方法是把当前的迭代变量作为匿名goroutine的参数。

package main

import (
    "fmt"
    "time"
)

func main() {
    data := []string{"one", "two", "three"}
    for _, v := range data {
        go func(in string) {
            fmt.Println(in)
        }(v)
    }
    time.Sleep(3 * time.Second)
    //goroutines print: one, two, three
}

  运行结果:

three
one
two

  下面这个陷阱稍微复杂一些的版本。

package main

import (
    "fmt"
    "time"
)

type field struct {
    name string
}

func (p *field) print() {
    fmt.Println(p.name)
}
func main() {
    data := []field{{"one"}, {"two"}, {"three"}}
    for _, v := range data {
        go v.print()
    }
    time.Sleep(3 * time.Second)
    //goroutines print: three, three, three
}

  运行结果:

three
three
three
package main

import (
    "fmt"
    "time"
)

type field struct {
    name string
}

func (p *field) print() {
    fmt.Println(p.name)
}
func main() {
    data := []field{{"one"}, {"two"}, {"three"}}
    for _, v := range data {
        v := v
        go v.print()
    }
    time.Sleep(3 * time.Second)
    //goroutines print: one, two, three
}

  运行结果:

three
one
two

  在运行这段代码时你认为会看到什么结果?(原因是什么?)

package main

import (
    "fmt"
    "time"
)

type field struct {
    name string
}

func (p *field) print() {
    fmt.Println(p.name)
}
func main() {
    data := []*field{{"one"}, {"two"}, {"three"}}
    for _, v := range data {
        go v.print()
    }
    time.Sleep(3 * time.Second)
}

  运行结果:

three
one
two

  46、Defer函数调用参数的求值

  被defer的函数的参数会在defer声明时求值(而不是在函数实际执行时)。

  Arguments for a deferred function call are evaluated when the defer statement is evaluated (not when the function is actually executing).

package main

import "fmt"

func main() {
    var i int = 1
    defer fmt.Println("result =>", func() int { return i * 2 }())
    i++
    //prints: result => 2 (not ok if you expected 4)
}

  运行结果:

result => 2

  47、被Defer的函数调用执行

  被defer的调用会在包含的函数的末尾执行,而不是包含代码块的末尾。对于Go新手而言,一个很常犯的错误就是无法区分被defer的代码执行规则和变量作用规则。如果你有一个长时运行的函数,而函数内有一个for循环试图在每次迭代时都defer资源清理调用,那就会出现问题。


package main

import (
    "fmt"
    "os"
    "path/filepath"
)
func main() {
    if len(os.Args) != 2 {
        os.Exit(-1)
    }
    start, err := os.Stat(os.Args[1])
    if err != nil || !start.IsDir() {
        os.Exit(-1)
    }
    var targets []string
    filepath.Walk(os.Args[1], func(fpath string, fi os.FileInfo, err error) error {
        if err != nil {
            return err
        }
        if !fi.Mode().IsRegular() {
            return nil
        }
        targets = append(targets, fpath)
        return nil
    })
    for _, target := range targets {
        f, err := os.Open(target)
        if err != nil {
            fmt.Println("bad target:", target, "error:", err) //prints error: too many open files
            break
        }
        defer f.Close() //will not be closed at the end of this code block
        //do something with the file...
    }
}

  运行结果:

exit status 255

  解决这个问题的一个方法是把代码块写成一个函数。


package main

import (
    "fmt"
    "os"
    "path/filepath"
)

func main() {
    if len(os.Args) != 2 {
        os.Exit(-1)
    }
    start, err := os.Stat(os.Args[1])
    if err != nil || !start.IsDir() {
        os.Exit(-1)
    }
    var targets []string
    filepath.Walk(os.Args[1], func(fpath string, fi os.FileInfo, err error) error {
        if err != nil {
            return err
        }
        if !fi.Mode().IsRegular() {
            return nil
        }
        targets = append(targets, fpath)
        return nil
    })
    for _, target := range targets {
        func() {
            f, err := os.Open(target)
            if err != nil {
                fmt.Println("bad target:", target, "error:", err)
                return
            }
            defer f.Close() //ok
            //do something with the file...
        }()
    }
}

  另一个方法是去掉defer语句

  48、失败的类型断言

  失败的类型断言返回断言声明中使用的目标类型的“零值”。这在与隐藏变量混合时,会发生未知情况。

package main

import "fmt"

func main() {
    var data interface{} = "great"
    if data, ok := data.(int); ok {
        fmt.Println("[is an int] value =>", data)
    } else {
        fmt.Println("[not an int] value =>", data)
        //prints: [not an int] value => 0 (not "great")
    }
}

  运行结果:

[not an int] value => 0
package main

import "fmt"

func main() {
    var data interface{} = "great"
    if res, ok := data.(int); ok {
        fmt.Println("[is an int] value =>", res)
    } else {
        fmt.Println("[not an int] value =>", data)
        //prints: [not an int] value => great (as expected)
    }
}

  运行结果:

[not an int] value => great

  49、阻塞的Goroutine和资源泄露

  Rob Pike在2012年的Google I/O大会上所做的“Go Concurrency Patterns”的演讲上,说道过几种基础的并发模式。从一组目标中获取第一个结果就是其中之一。

func First(query string, replicas ...Search) Result {
    c := make(chan Result)
    searchReplica := func(i int) { c <- replicas[i](query) }
    for i := range replicas {
        go searchReplica(i)
    }
    return <-c
}

  这个函数在每次搜索重复时都会起一个goroutine。每个goroutine把它的搜索结果发送到结果的channel中。结果channel的第一个值被返回。

  那其他goroutine的结果会怎样呢?还有那些goroutine自身呢?

  在First()函数中的结果channel是没缓存的。这意味着只有第一个goroutine返回。其他的goroutine会困在尝试发送结果的过程中。这意味着,如果你有不止一个的重复时,每个调用将会泄露资源。

  为了避免泄露,你需要确保所有的goroutine退出。一个不错的方法是使用一个有足够保存所有缓存结果的channel。

func First(query string, replicas ...Search) Result {
    c := make(chan Result, len(replicas))
    searchReplica := func(i int) { c <- replicas[i](query) }
    for i := range replicas {
        go searchReplica(i)
    }
    return <-c
}

  另一个不错的解决方法是使用一个有default情况的select语句和一个保存一个缓存结果的channel。default情况保证了即使当结果channel无法收到消息的情况下,goroutine也不会堵塞。

func First(query string, replicas ...Search) Result {
    c := make(chan Result, 1)
    searchReplica := func(i int) {
        select {
        case c <- replicas[i](query):
        default:
        }
    }
    for i := range replicas {
        go searchReplica(i)
    }
    return <-c
}

  你也可以使用特殊的取消channel来终止workers。

func First(query string, replicas ...Search) Result {
    c := make(chan Result)
    done := make(chan struct{})
    defer close(done)
    searchReplica := func(i int) {
        select {
        case c <- replicas[i](query):
        case <-done:
        }
    }
    for i := range replicas {
        go searchReplica(i)
    }
    return <-c
}

  为何在演讲中会包含这些bug?Rob Pike仅仅是不想把演示复杂化。这么作是合理的,但对于Go新手而言,可能会直接使用代码,而不去思考它可能有问题。

  老男孩GO语言课程率行业之先开设,并进行多次迭代升级,以帮助学员学习到真正有用的知识,如有学习需求,可以关注“GO语言开发”课程。

  推荐阅读:

  Go语言错误总结(六)!老男孩IT教育

  为什么要学习GO语言?老男孩GO培训

  Go语言错误总结(一)!老男孩GO语言学习周期

本文经授权发布,不代表老男孩教育立场。如若转载请联系原作者。