30

细数在用golang&beego做api server过程中的坑(一)

 5 years ago
source link: https://studygolang.com/articles/16986?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在介绍之前先说明一下,标题中带有【beego】标签的,是beego框架使用中遇到的坑。如果没有,那就是golang本身的坑。当然,此坑并非人家代码有问题,有几个地方反而是出于性能等各方面的考量有意而为之。但这些地方却是一门语言或框架的初学者大概率会遇到的困惑。

1、slice/map遍历时的修改问题

在go中,slice/map的迭代循环的常用方式为:

slice:
for index,value := range _slice {
}
map:
for key,value := range _map {
}

这其中value就是遍历时的当前值。

按照我们之前在java中的遍历习惯,当遍历到第几个时,拿到的就是指向第几个对象的引用,因此对对象的所有修改行为本质上修改的都是原值。但是在这里并不是。看一个例子:

//tag1
var structs = []testModel{
        testModel{
            A: "一",
            B: "一一",
            C: 1,
        },
        testModel{
            A: "二",
            B: "二二",
            C: 2,
        },
        testModel{
            A: "三",
            B: "三三",
            C: 3,
        },
    }
//tag2
    for _, val := range structs {
        val.A = "四"
        val.B = "四四"
        val.C = 4
    }
//tag3
    for _, val := range structs {
        fmt.Print(val.A)
        fmt.Print(val.B)
        fmt.Println(val.C)
    }

代码逻辑很简单。

  • 我们在tag1处定义了一个数组,里面包含三个testModel实例。每个testModel实例有三个字段,并且他们的字段值都是互不相同的。
  • 按照事先的想法,是要在tag2处将所有testModel实例的字段值修改为相同的。
  • tag3检查一下修改结果。
    结果console输出如下;
=== RUN   TestLogic
一一一1
二二二2
三三三3
--- PASS: TestLogic (0.00s)
PASS

打印的仍然是旧值。那这是为什么呢?

原因是:在range遍历时,map中的key&value,slice中的index&value,都是新的临时变量,这个临时变量被每一次迭代所共用,临时变量的值也是由被遍历元素复制而来。因此在该变量上修改是无效的。

那如何才能有效呢? 很简单,对上例的tag2部分做如下修改:

for key, _ := range structs {
        structs[key].A = "四"
        structs[key].B = "四"
        structs[key].C = 4
    }

同理Slice修改时也需要用slice[index].column = newValue 的方式进行。

2、map/slice遍历时多协程问题(multi-goroutines)

在map遍历时,我们有可能会在map中使用go routines进行一些操作,例如下面这个例子:

var values = []int{1, 2, 3, 4, 5, 6, 7, 8, 9}

    var block = make(chan int, 2)
//wrong
    for i, val := range values {
        go func() {
            fmt.Println(1000 + val)
            if i == len(values)-1 {
                block <- 1
            }
        }()
    }
    for i := 0; i < 2; i++ {
        <-block
    }
}

我们想用map中迭代的当前值,在协程中做一番大事业,潜意识的写法可能就是按照wrong中的写法那样,直接把value拿过来就用。但是却得到这样的结果:

=== RUN   TestLogic
1009
1009
1009
1009
1009
1009
1009
1009
1009
--- PASS: TestLogic (0.00s)
PASS

也就是说,在goroutine中的val,值竟然都是map遍历的最后一个!导致这一现象的原因有两个:

  • for range下的迭代变量val的值是共用的,这一点在《slice/map遍历时修改问题 》中有提到
  • main函数所在的goroutine和后续启动的goroutines存在竞争关系
    为了证实这一点,修改代码为如下:
for i, val := range values {
        fmt.Println(&val)
        go func() {
            fmt.Println(1000 + val)
            if i == len(values)-1 {
                block <- 1
            }
        }()
    }

加了一行代码,打印val的内存地址,结果如下:

0xc000287738
0xc000287738
0xc000287738
0xc000287738
0xc000287738
0xc000287738
0xc000287738
0xc000287738
0xc000287738
1005
1009
1009
1009
1009
1009

val的地址在每次遍历时是同一个!证明第一点;goroutines在不同的遍历中存在变化,例如1005,证明第二点;当然也可用go run -trace ***.go 命令来查看协程的变化,就不多赘述。

那如何修改呢?只需要使用 函数参数复制 做一次数据复制即可,而不是闭包:

for i, val := range values {
        go func(val int) {
            fmt.Println(2000 + val)
            if i == len(values)-1 {
                block <- 1
            }
        }(val)
    }

关于map遍历时多协程并发问题也可参考: https://github.com/golang/go/wiki/CommonMistakes

3、数组与值拷贝

首先把结论放在这,然后再展开讨论:

go语言数组的一切传递都是值拷贝,包括但不限于以下三个方面:

  • 1、数组之间的直接赋值。
  • 2、数组作为函数参数。
  • 3、数组内嵌到struct中。

数组之间的直接赋值

看下面一段代码:

a := []int{1,2,3}

    //值复制
    b := a
    fmt.Printf("%p, %v\n", &a, a) //0xc0000bf660, [1 2 3]
    fmt.Printf("%p, %v\n", &b, b) //0xc0000bf680, [1 2 3]

    a = append(a, 4)
    a[0] = 4
    fmt.Println(len(b))//3
    for e := range a {//0123
        fmt.Print(e)
    }

首先定义了一个数组a,a中有3个元素。然后通过一次赋值操作,将a赋值给了b。

在java中,数组之间的赋值,是引用的传递,在a中修改后再通过b进行打印输出,会得到修改后的值。但是刚才说过,go中数组之间的复制操作是值拷贝。因此打印b仍然还是修改前的样子,会发现a和b在内存中是完全不同的两块内存区域。

数组作为函数参数传递

因为golang中函数参数的传递都是值拷贝,因此这一点放在数组上也不难理解。

在如上代码添加一句:test4(a)

a := []int{1,2,3}

    //值复制
    b := a

    fmt.Printf("%p, %v\n", &a, a) //0xc0000bf660, [1 2 3]
    fmt.Printf("%p, %v\n", &b, b) //0xc0000bf680, [1 2 3]

    a = append(a, 4)
    a[0] = 4
    fmt.Println(len(b))//3
    for e := range a {//0123
        fmt.Print(e)
    }

    test4(a)

其中test4代码如下:

func test4(param []int) {
    fmt.Printf("%p, %v\n", &param, param) //01230xc0000bf700, [4 2 3 4]
}

会发现a & b & c各有各的内存地址~~

数组内嵌到struct中

a := []string{"1", "22"}
    var c = struct {
        S []string
    }{
        S: []string{"1", "22"},
    }

    //结构是值拷贝,内部的数组也是值拷贝
    b := c

    //修改c中的数组元素值不影响b
    c.S[0] = "2"

    //修改b中的数组元素不影响c
    b.S[0] = "3"

    //地址不相同,说明每一个变量在内存中是独立的内存区域
    fmt.Printf("%p,%v\n", &a, a)     //0xc0000bd660,[1 22]
    fmt.Printf("%p,%v\n", &b.S, b.S) //0xc0000bd720,[3 22]
    fmt.Printf("%p,%v\n", &c.S, c.S) //0xc0000bd6a0,[3 22]

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK