前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go语言常见坑

Go语言常见坑

作者头像
我的小碗汤
发布2018-08-22 11:03:08
4770
发布2018-08-22 11:03:08
举报
文章被收录于专栏:我的小碗汤我的小碗汤

这里列举的Go语言常见坑都是符合Go语言语法的, 可以正常的编译, 但是可能是运行结果错误, 或者是有资源泄漏的风险。

数组是值传递

在函数调用参数中, 数组是值传递, 无法通过修改数组类型的参数返回结果。

代码语言:javascript
复制
package main

import "fmt"

func main() {
  x := [3]int{1, 2, 3}

  func(arr [3]int) {
    arr[0] = 7
    fmt.Println(arr)
  }(x)

  fmt.Println(x)
}

运行程序,结果如下:

代码语言:javascript
复制
[7 2 3]
[1 2 3]

Process finished with exit code 0

在go中最好使用切片,因为切片是引用传递。这个和java是有区别的,java如下:

代码语言:javascript
复制
import java.util.Arrays;

public class Hello {
  public static void main(String[] args) {
    String[] arr = {"1","2","3"};
    test(arr);
    System.out.println("main arr is " + Arrays.asList(arr));
  }
  
  public static void test(String[] arr){
    arr[0] = "10";
    System.out.println("test arr is " + Arrays.asList(arr));
  }
}

运行结果如下:

代码语言:javascript
复制
test arr is [10, 2, 3]
main arr is [10, 2, 3]

map遍历是顺序不固定

map是一种hash表实现, 每次遍历的顺序都可能不一样。

代码语言:javascript
复制
package main

func main() {
  m := map[string]string{
    "1": "1",
    "2": "2",
    "3": "3",
  }

  for k, v := range m {
    println(k, v)
  }
  println("--------------------------------------------")
  for k, v := range m {
    println(k, v)
  }
}

输出结果如下:

代码语言:javascript
复制
2 2
3 3
1 1
--------------------------------------------
1 1
2 2
3 3

Process finished with exit code 0

返回值被屏蔽

在局部作用域中, 命名的返回值内同名的局部变量屏蔽:

代码语言:javascript
复制
func Foo() (err error) {
  if err := Bar(); err != nil {
    return
  }
  return
}

recover必须在defer函数中运行

  • recover捕获的是祖父级调用时的异常, 直接调用时无效:
代码语言:javascript
复制
func main() {  
  recover()
  panic(1)
}
  • 直接defer调用也是无效:
代码语言:javascript
复制
func main() {  
  defer recover()
  panic(1)
}
  • defer调用时多层嵌套依然无效:
代码语言:javascript
复制
func main() {
  defer func() {
    func() {
      recover()
    }()
  }()
  panic(1)
}
  • 必须在defer函数中直接调用才有效:
代码语言:javascript
复制
package main

func main() {
  defer func() {
    recover()
  }()
  panic(1)
}

main函数提前退出

后台Goroutine无法保证完成任务,这里hello不会打印出来:

代码语言:javascript
复制
func main() {
  go println("hello")
}

通过Sleep来回避并发中的问题

休眠并不能保证输出完整的字符串:

代码语言:javascript
复制
func main() {
  go println("hello")
  time.Sleep(time.Second)
}

类似的还有通过插入调度语句,当字符串长度很长时,也不会输出。

代码语言:javascript
复制
func main() {
  go println("hello")
  runtime.Gosched()
}
代码语言:javascript
复制
    }
  }()

  for {
    runtime.Gosched()
  }
}

Goroutine间不满足顺序一致性内存模型

因为在不同的Goroutine, main函数可能无法观测到done的状态变化, 那么for循环会陷入死循环:

代码语言:javascript
复制
package main

import (
  "runtime"
)
var msg string
var done bool = false

func main() {
  runtime.GOMAXPROCS(1)

  go func() {
    msg = "hello, world"
    done = true
  }()

  for {
    if done {
      println(msg)
      break
    }
  }
}

解决的办法是用显式同步:

代码语言:javascript
复制
package main

import (
  "runtime"
)

var msg string
var done = make(chan bool)

func main() {
  runtime.GOMAXPROCS(1)

  go func() {
    msg = "hello, world"
    done <- true
  }()

  <-done
  println(msg)
}

闭包错误引用同一个变量

如下代码,最后只会输出五次5:

代码语言:javascript
复制
package main

func main() {
  for i := 0; i < 5; i++ {
    defer func() {
      println(i)
    }()
  }
}

改进的方法是在每轮迭代中生成一个局部变量

代码语言:javascript
复制
package main

func main() {
  for i := 0; i < 5; i++ {
    i := i
    defer func() {
      println(i)
    }()
  }
}

或者是通过函数参数传入:

代码语言:javascript
复制
package main

func main() {
  for i := 0; i < 5; i++ {
    defer func(i int) {
      println(i)
    }(i)
  }
}

在循环内部执行defer语句

defer在函数退出时才能执行, 在for执行defer会导致资源延迟释放:

代码语言:javascript
复制
func main() {
  for i := 0; i < 5; i++ {
    f, err := os.Open("/path/to/file")
    if err != nil {
      log.Fatal(err)
    }
    defer f.Close()
  }
}

解决的方法可以在for中构造一个局部函数, 在局部函数内部执行defer:

代码语言:javascript
复制
func main() {
  for i := 0; i < 5; i++ {
    func() {
      f, err := os.Open("/path/to/file")
      if err != nil {
        log.Fatal(err)
      }
      defer f.Close()
    }()
  }
}

切片会导致整个底层数组被锁定

切片会导致整个底层数组被锁定, 底层数组无法释放内存. 如果底层数组较大会对内存产生很大的压力.

代码语言:javascript
复制
func main() {
  headerMap := make(map[string][]byte)

  for i := 0; i < 5; i++ {
    name := "/path/to/file"
    data, err := ioutil.ReadFile(name)
    if err != nil {
      log.Fatal(err)
    }
    headerMap[name] = data[:1]
  }

  // do some thing
}

解决的方法是将结果克隆一份, 这样可以释放底层的数组:

代码语言:javascript
复制
func main() {
  headerMap := make(map[string][]byte)

  for i := 0; i < 5; i++ {
    name := "/path/to/file"
    data, err := ioutil.ReadFile(name)
    if err != nil {
      log.Fatal(err)
    }
    headerMap[name] = append([]byte{}, data[:1]...)
  }

  // do some thing
}

空指针和空接口不等价

比如返回了一个错误指针, 但并不是空的error接口,昨天的分享中说到,接口作为两个元素实现:一个类型和一个值,所以这里返回的值{*MyError,p}

代码语言:javascript
复制
func returnsError() error {
  var p *MyError = nil
  if bad() {
    p = ErrBad
  }
  return p // Will always return a non-nil error.
}

内存地址会变化

Go语言中对象的地址可能发生变化, 因此指针不能从其它非指针类型的值生成:

代码语言:javascript
复制
func main() {
  var x int = 42
  var p uintptr = uintptr(unsafe.Poiner(&x))

  runtime.GC()
  var px *int = (*int)(unsafe.Poiner(p))
  println(*px)
}

当内存发送变化的时候, 相关的指针会同步更新, 但是非指针类型的uintptr不会做同步更新.

同理, cgo中也不能保存Go对象地址.

Goroutine泄露

Go语言是带内存自动回收的特性,因此内存一般不会泄漏。但是Goroutine确存在泄漏的情况,同时泄漏的Goroutine引用的内存同样无法被回收。

代码语言:javascript
复制
func main() {
  ch := func() <-chan int {
    ch := make(chan int)
    go func() {
      for i := 0; ; i++ {
        ch <- i
      }
    } ()
    return ch
  }()
  
  for v := range ch {
    fmt.Println(v)
    if v == 5 {
      break
    }
  }
}

上面的程序中后台Goroutine向管道输入自然数序列,main函数中输出序列。但是当break跳出for循环的时候,后台Goroutine就处于无法被回收的状态了。

我们可以通过context包来避免这个问题:

代码语言:javascript
复制
func main() {
  ctx, cancel := context.WithCancel(context.Background())
  
  ch := func(ctx context.Context) <-chan int {
    ch := make(chan int)
    go func() {
      for i := 0; ; i++ {
        select {
        case <- ctx.Done():
          return
        case ch <- i:
        }
      }
    } ()
    return ch
  }(ctx)
  
  for v := range ch {
    fmt.Println(v)
    if v == 5 {
      cancel()
      break
    }
  }
}

当main函数在break跳出循环时,通过调用cancel()来通知后台Goroutine退出,这样就避免了Goroutine的泄漏。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-06-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 进击云原生 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档