go 语法常犯错误

2018-07-16  本文已影响41人  Jancd

不小心覆盖了变量

对从动态语言转过来的开发者来说,简短声明很好用,这可能会让人误会 := 是一个赋值操作符。

如果你在新的代码块中像下边这样误用了 :=,编译不会报错,但是变量不会按你的预期工作:

func main() {
    x := 1
    println(x)        // 1
    {
        println(x)    // 1
        x := 2
        println(x)    // 2    // 新的 x 变量的作用域只在代码块内部
    }
    println(x)        // 1
}

这是 Go 开发者常犯的错,而且不易被发现。

这是 Go 开发者常犯的错,而且不易被发现。

可使用 vet 工具来诊断这种变量覆盖,Go 默认不做覆盖检查,添加 -shadow 选项来启用:

> go tool vet -shadow main.go
main.go:9: declaration of "x" shadows declaration at main.go:5

注意 vet 不会报告全部被覆盖的变量,可以使用 go-nyet 来做进一步的检测:

> $GOPATH/bin/go-nyet main.go
main.go:10:3:Shadowing variable `x`

map 容量

在创建 map 类型的变量时可以指定容量,但不能像 slice 一样使用 cap() 来检测分配空间的大小:

// 错误示例
func main() {
    m := make(map[string]int, 99)
    println(cap(m))     // error: invalid argument m1 (type map[string]int) for cap  
}    

类型声明与方法

从一个现有的非 interface 类型创建新类型时,并不会继承原有的方法:

// 定义 Mutex 的自定义类型
type myMutex sync.Mutex

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

mtx.Lock undefined (type myMutex has no field or method Lock)...

如果你需要使用原类型的方法,可将原类型以匿名字段的形式嵌到你定义的新 struct 中:

// 类型以字段形式直接嵌入
type myLocker struct {
    sync.Mutex
}

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

range 遍历 slice 和 array 时混淆了返回值

与其他编程语言中的 for-in 、foreach 遍历语句不同,Go 中的 range 在遍历时会生成 2 个值,第一个是元素索引,第二个是元素的值:

// 错误示例
func main() {
    x := []string{"a", "b", "c"}
    for v := range x {
        fmt.Println(v)    // 1 2 3
    }
}
// 正确示例
func main() {
    x := []string{"a", "b", "c"}
    for _, v := range x {    // 使用 _ 丢弃索引
        fmt.Println(v)
    }
}

访问 map 中不存在的 key

和其他编程语言类似,如果访问了 map 中不存在的 key 则希望能返回 nil.

Go 则会返回元素对应数据类型的零值,比如 nil、'' 、false 和 0,取值操作总有值返回,故不能通过取出来的值来判断 key 是不是在 map 中。

检查 key 是否存在可以用 map 直接访问,检查返回的第二个参数即可:

// 错误的 key 检测方式
func main() {
    x := map[string]string{"one": "2", "two": "", "three": "3"}
    if v := x["two"]; v == "" {
        fmt.Println("key two is no entry")    // 键 two 存不存在都会返回的空字符串
    }
}
// 正确示例
func main() {
    x := map[string]string{"one": "2", "two": "", "three": "3"}
    if _, ok := x["two"]; !ok {
        fmt.Println("key two is no entry")
    }
}

defer 函数的参数值

对 defer 延迟执行的函数,它的参数会在声明时候就会求出具体值,而不是在执行时才求值:

// 在 defer 函数中参数会提前求值
func main() {
    var i = 1
    defer fmt.Println("result: ", func() int { return i * 2 }())
    i++
}

result: 2

defer 函数的执行时机

对 defer 延迟执行的函数,会在调用它的函数结束时执行,而不是在调用它的语句块结束时执行,注意区分开。

比如在一个长时间执行的函数里,内部 for 循环中使用 defer 来清理每次迭代产生的资源调用,就会出现问题.

package main

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

// 命令行参数指定目录名
// 遍历读取目录下的文件
func main() {
    if len(os.Args) != 2 {
        os.Exit(1)
    }

    dir := os.Args[1]
    start,err := os.Stat(dir)
    if err != nil && !start.IsDir() {
        os.Exit(2)
    }

    var targets []string
    filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
        if err != nil{
            return err
        }

        if !info.Mode().IsRegular() {
            return nil
        }

        targets = append(targets,path)

        return nil
    })

    for _,target := range targets{
        func(){
            f,err := os.Open(target)
            if err != nil {
                fmt.Println("bad target:", target, "error:", err)
                return    // 在匿名函数内使用 return 代替 break 即可
            }
            fmt.Println("ok target:", target)
            defer f.Close()
        }()
    }
}

for 语句中的迭代变量与闭包函数

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 three three 
}


// 正确示例
func main() {
    data := []field{{"one"}, {"two"}, {"three"}}
    for _, v := range data {
        v := v
        go v.print()
    }
    time.Sleep(3 * time.Second)
    // 输出 one two three
}

// 正确示例
func main() {
    data := []*field{{"one"}, {"two"}, {"three"}}
    for _, v := range data {    // 此时迭代值 v 是三个元素值的地址,每次 v 指向的值不同
        go v.print()
    }
    time.Sleep(3 * time.Second)
    // 输出 one two three
}

类型声明与方法

从一个现有的非 interface 类型创建新类型时,并不会继承原有的方法:

// 定义 Mutex 的自定义类型
type myMutex sync.Mutex

func main() {
    var mtx myMutex
    mtx.Lock()
    mtx.UnLock()
}
> mtx.Lock undefined (type myMutex has no field or method Lock)...

如果你需要使用原类型的方法,可将原类型以匿名字段的形式嵌到你定义的新 struct 中:

// 类型以字段形式直接嵌入
type myLocker struct {
    sync.Mutex
}

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

interface 类型声明也保留它的方法集:

type myLocker sync.Locker

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

使用指针作为方法的 receiver

只要值是可寻址的,就可以在值上直接调用指针方法。即是对一个方法,它的 receiver 是指针就足矣。

但不是所有值都是可寻址的,比如 map 类型的元素、通过 interface 引用的变量:

type data struct {
    name string
}

type printer interface {
    print()
}

func (p *data) print() {
    fmt.Println("name: ", p.name)
}

func main() {
    d1 := data{"one"}
    d1.print()    // d1 变量可寻址,可直接调用指针 receiver 的方法

    var in printer = data{"two"}
    in.print()    // 类型不匹配

    m := map[string]data{
        "x": data{"three"},
    }
    m["x"].print()    // m["x"] 是不可寻址的    // 变动频繁
}
上一篇下一篇

猜你喜欢

热点阅读