女主宣言

前言:在使用Golang开发项目的过程中,我们的攻城狮遇到了4个看似不起眼的小问题,但是排查起来确实耗费了不少时间。快来看看你是不是也遇到过吧,希望这篇文章能帮助到你。

PS:丰富的一线技术、多元化的表现形式,尽在“360云计算”,点关注哦!

1

循环变量怎么不变?

请看下面的代码,你觉得输出结果会是啥?Values是“1 2 3”,Addresses是三个不同的地址?

package mainimport "fmt"func main() {in := []int{1, 2, 3}var out []*intfor _, v := range in {out = append(out, &v)}fmt.Println("Values:", *out[0], *out[1], *out[2])fmt.Println("Addresses:", out[0], out[1], out[2])
}

不是的,运行结果是:

Values: 3 3 3
Addresses: 0xc000086010 0xc000086010 0xc000086010

为什么out切片里的值都是3?每次迭代都是append变量v的地址到out,从上面的输出结果看出,每个元素的地址是同一个,说明每次迭代用的都是同一个变量v,只是被赋了新的值而已,for循环执行完的时候,goroutine里的变量v刚好处于切片in里的某个值,于是就出现了上面的这种结果。

怎么修复呢?最简单的方法只需添加一行代码,把循环变量copy到一个新的变量:

package mainimport "fmt"func main() {in := []int{1, 2, 3}var out []*intfor _, v := range in {v := v     // 把v的值复制给新的变量vout = append(out, &v)}fmt.Println("Values:", *out[0], *out[1], *out[2])fmt.Println("Addresses:", out[0], out[1], out[2])
}

这样便会得到我们预想的输出:

Values: 1 2 3
Addresses: 0xc000096010 0xc000096018 0xc000096020

虽然“v:=v”看起来不那么易读,但是确实很有效。相当于创建了另一个名为v的变量实例,当然也可以换成其他名称。

还有一种改法,按照切片in中索引取地址append到out里,for-range部分修改如下:

for i := range in {out = append(out, &in[i])
}

同样的情况也会出现在循环中使用goroutine的时候:

package mainimport ("fmt""sync"
)func main() {in := []int{1, 2, 3}var out []*intwg := sync.WaitGroup{}for _, v := range in {wg.Add(1)go func() {defer wg.Done()out = append(out, &v)}()}wg.Wait()fmt.Println("Values:", *out[0], *out[1], *out[2])fmt.Println("Addresses:", out[0], out[1], out[2])
}

输出结果将会是:

Values: 3 3 3
Addresses: 0xc000014098 0xc000014098 0xc000014098

因为闭包只绑定到变量v,整个for循环中变量v对应的又都是同一个内存地址,每次循环只是改了这个地址上的值,所以总会是打印 in 的最后一个元素的值。解决方法是将 v 拷贝到一个新的变量:

for _, v := range in {wg.Add(1)item := vgo func() {defer wg.Done()out = append(out, &item)}()
}

或者作为参数传到goroutine中:

for _, v := range in {wg.Add(1)go func(value int) {defer wg.Done()out = append(out, &value)}(v)
}

2

当心 := 的作用域

golang 有两个赋值运算符“=”和“:=”,区别不再多说。尽管“:=”很方便,但是在不同作用域和多返回值的情况下会出现预料之外的结果。请看下面一段代码:

package mainimport ("fmt"
)func getUsers() ([]string, error) {return []string{"小赵","小钱","小孙","小李"}, nil
}func main() {var users []stringusers, err := getUsers()if err != nil {panic("ERROR!")}for _, user:= range users {fmt.Println(user)}
}

这个例子中,我们从某处拿到用户名并打印,输出结果是:

小赵小钱小孙小李

注意 := 的用法:

users, err := getUsers()

users已经在之前声明了,但是仍然可以用 := 声明并赋值,这是因为err并没有提前声明。然后,再稍微修改一下代码:

package mainimport ("fmt""os"
)func getUsers() ([]string, error) {return []string{"小赵", "小钱", "小孙", "小李"}, nil
}func main() {var users = make([]string, 0)envUsers := os.Getenv("USERS")if envUsers == "" {fmt.Println("Get users from db")users, err := getUsers()if err != nil {panic("ERROR!")}fmt.Println("Users total: ", len(users))}for _, user := range users {fmt.Println(user)}
}

你感觉这次会输出什么呢?结果是:

Get users from db
Users total:  4

这就是 := 的作用域问题。Golang中用“{}”定义一个作用域,在这里 if 语句创建了一个新的作用域,当使用:= 时,会把 users和err都当做这个作用域里新的变量,当作用域关闭的时候,users和err也会被丢弃。

这段代码也有一些实际的应用场景,例如在初始化流程中,通过环境变量控制系统的某些行为,上面的实现会导致系统初始化数据错误。那怎么解决这个问题呢?

func main() {var users = make([]string, 0)var err error     // 在这里声明 err 变量,下面就不用 := 赋值了envUsers := os.Getenv("USERS")if envUsers == "" {fmt.Println("Get users from db")users, err = getUsers()if err != nil {panic("ERROR!")}fmt.Println("Users total: ", len(users))}for _, user := range users {fmt.Println(user)}
}

得到的结果将会是我们期望的:

Get users from dbUsers total:  4小赵小钱小孙小李

尤其在修改旧代码的时候,往往会忽略代码的作用域,这一点是需要特别注意的。

3

能无限的使用goroutine吗?

请看下面一段代码:

package mainimport ("fmt""sync""time"
)type A struct {id int
}func main() {start := time.Now()channel := make(chan A, 100)var wg sync.WaitGroupwg.Add(1)go func() {defer wg.Done()for a := range channel {process(a)}}()for i := 0; i < 100; i++ {channel <- A{id: i}}close(channel)wg.Wait()cost:= time.Since(start)fmt.Printf("Took %s\n", cost)
}func process(a A) {fmt.Printf("Start processing %v\n", a)time.Sleep(100 * time.Millisecond)fmt.Printf("Finish processing %v\n", a)
}

这段代码里,我们定义了一个channel,遍历这个channel,从中获取数据并进行比较耗时的处理。这段代码运行了10秒左右,假如有10万条数据,那会运行将近3个小时。下面我们再使用goroutine修改下代码:

package mainimport ("fmt""sync""time"
)type A struct {id int
}func main() {start := time.Now()channel := make(chan A, 100)var wg sync.WaitGroupwg.Add(1)go func() {defer wg.Done()for a := range channel {wg.Add(1)go func(a A) {defer wg.Done()process(a)}(a)}}()for i := 0; i < 100; i++ {channel <- A{id: i}}close(channel)wg.Wait()cost := time.Since(start)fmt.Printf("Took %s\n", cost)
}func process(a A) {fmt.Printf("Start processing %v\n", a)time.Sleep(100 * time.Millisecond)fmt.Printf("Finish processing %v\n", a)
}

为了发挥go的并发处理的优势,提高处理速度,在每个循环里调度了一个 goroutine 去处理数据。这样的确速度快了10倍。

但是如果channel里数据量较大,假如有10万条数据,也能这样处理吗?答案是:要视情况而定。

想弄明白为什么,就要先知道当调用一个goroutine时都发生了些什么。简单来说,golang的runtime会分配一个包含了这个goroutine所有相关数据的对象,当执行完这个goroutine的时候,才会被释放。一个goroutine对象至少会占用2k的内存空间,但是在64位的机器上也能达到1GB。越多的goroutine意味着越多的内存占用,另外goroutine实际是在cpu上执行的,更少的cpu核数,也会导致更多的对象占用内存等待被执行。

所以解决办法是,增加一个协程池,控制goroutine并发数量,保持内存在一个可控的范围内。

package mainimport ("fmt""sync""time"
)type A struct {id int
}func main() {start := time.Now()workerPoolSize := 100channel := make(chan A, 100)var wg sync.WaitGroupwg.Add(1)go func() {defer wg.Done()for i := 0; i < workerPoolSize; i++ {wg.Add(1)go func() {defer wg.Done()for a := range channel {process(a)}}()}}()// Feeding the channelfor i := 0; i < 100000; i++ {channel <- A{id: i}}close(channel)wg.Wait()cost := time.Since(start)fmt.Printf("Took %s\n", cost)
}func process(a A) {fmt.Printf("Start processing %v\n", a)time.Sleep(100 * time.Millisecond)fmt.Printf("Finish processing %v\n", a)
}

这段代码里workerPoolSize既是goroutine的最大并发数量。可以把channel当做一个队列,每个goroutine都是一个消费者。golang允许多个goroutine监听同一个channel,但是channel里的每个元素都只会被处理一次。

workerPoolSize 应该设置成可配置的(例如在环境变量里,或者配置文件里),这样可以根据服务器性能不同,控制并发数量,以提高资源利用率。workerPoolSize应该小于或等于可分配总内存和单个goroutine大小的比值。

4

结构体成员的排列顺序不同,居然会影响占用的内存空间?

这就要说到struct的内存对齐了。

例如下面两个struct:

type BadOrderedUser struct {IsLocked     bool   // 1 byteName         string // 16 byteID           int32  // 4 byte
}type OrderedUser struct {Name        stringID          int32IsLocked    bool
}

表面上来看这俩个结构体应该都是21 bytes大小,然而实际没这么简单。在64位系统上, BadOrderedUser占用了32 bytes的内存空间,OrderedUser却只占用了 24 bytes。为什么呢?

CPU访问内存时,并不是逐个字节访问,而是以字长为单位访问,比如64位的CPU,字长为8字节,那么CPU访问内存的单位也是8字节。这么设计的目的,是减少 CPU 访问内存的次数,加大 CPU 访问内存的吞吐量。比如同样读取 16 个字节的数据,一次读取 8 个字节那么只需要读取 2 次。CPU 始终以字长访问内存,如果不进行内存对齐,很可能增加 CPU 访问内存的次数。例如下图:

变量 a、b 各占据 5 字节的空间,内存对齐后,a、b 占据 8 字节空间,CPU 读取 b 变量的值只需要进行一次内存访问。如果不进行内存对齐,CPU 读取 b 变量的值需要进行 2 次内存访问。第一次访问得到 b 变量的前3 个字节,第二次访问得到 b 变量的后2个字节。

go中的结构体内存布局和c结构体布局类似,每个成员的内存分布是连续的,所以在内存对齐过程中,成员的排列顺序不同,上一个成员因偏移量而浪费的大小也不同,导致最后结构体占用的内存空间不同。 一个结构体实例所占据的空间等于各成员占据空间之和,再加上内存对齐的空间大小。

下面代码可以查看结构体占用的空间大小,以及偏移量等信息:

package mainimport ("fmt""reflect""unsafe"
)type BadOrderedUser struct {IsLocked bool   // 1 byteName     string // 16 byteID       int32  // 4 byte
}type OrderedUser struct {Name     stringID       int32IsLocked bool
}func main() {fmt.Printf("BadOrderedUser size: %d\n", unsafe.Sizeof(BadOrderedUser{}))   // BadOrderedUser 占用空间大小typ := reflect.TypeOf(BadOrderedUser{})for i := 0; i < typ.NumField(); i++ {                                           // 每个字段的偏移量,大小和内存对齐倍数field := typ.Field(i)fmt.Printf("%s at offset %v, size=%d, align=%d\n",field.Name, field.Offset, field.Type.Size(), field.Type.Align())}fmt.Printf("OrderedUser size: %d\n", unsafe.Sizeof(OrderedUser{}))    // OrderedUser占用空间大小typ = reflect.TypeOf(OrderedUser{})for i := 0; i < typ.NumField(); i++ {field := typ.Field(i)fmt.Printf("%s at offset %v, size=%d, align=%d\n",field.Name, field.Offset, field.Type.Size(), field.Type.Align())}
}

运行结果为:

BadOrderedUser size: 32
IsLocked at offset 0, size=1, align=1
Name at offset 8, size=16, align=8
ID at offset 24, size=4, align=4
OrderedUser size: 24
Name at offset 0, size=16, align=8
ID at offset 16, size=4, align=4
IsLocked at offset 20, size=1, align=1

所以,如果在设计访问很频繁的大结构体的时候,可以通过调整字段的顺序,减少内存占用。

以上就是本次文章的所有内容了,如果你对我们的文章有任何见解,欢迎在下方留言交流哦。

关于Golang的4个小秘密相关推荐

  1. 【Golang源码分析】Go Web常用程序包gorilla/mux的使用与源码简析

    目录[阅读时间:约10分钟] 一.概述 二.对比: gorilla/mux与net/http DefaultServeMux 三.简单使用 四.源码简析 1.NewRouter函数 2.HandleF ...

  2. 基于Golang的简单web服务程序开发——CloudGo

    基于Golang的简单web服务程序开发--CloudGo[阅读时间:约10分钟] 一.概述 二.系统环境&项目介绍 1.系统环境 2.项目的任务要求 (1)基本要求 (2)扩展要求 三.具体 ...

  3. CentOS Docker安装配置部署Golang web helloworld

    目录[阅读时间:约5分钟] 一.Docker简介 二.Docker的安装与配置[CentOS环境] 三.Docker部署Golang web helloworld 四.Docker与虚拟机的区别 五. ...

  4. 【ReactiveX】基于Golang pmlpml/RxGo程序包的二次开发

    基于Golang pmlpml/RxGo程序包的二次开发[阅读时间:约20分钟] 一.ReactiveX & RxGo介绍 1.ReactiveX 2.RxGo 二.系统环境&项目介绍 ...

  5. 【golang程序包推荐分享】分享亿点点golang json操作及myJsonMarshal程序包开发的踩坑经历 :)

    目录[阅读时间:约5分钟] 一.概述 1.Json的作用 2.Go官方 encoding/json 包 3. golang json的主要操作 二.Json Marshal:将数据编码成json字符串 ...

  6. 基于Golang的对象序列化的程序包开发——myJsonMarshal

    基于Golang的对象序列化的程序包开发--myJsonMarshal[阅读时间:约10分钟] 一.对象序列化概述 二.系统环境&项目介绍 1.系统环境 2.项目的任务要求 三.具体程序设计及 ...

  7. 【golang程序包推荐分享】go-ini、viper、godoc

    [golang程序包推荐&分享]go-ini.viper.godoc 一.go-ini 1.程序包简介 2.下载安装 3.简单使用[截取自官网] 二.viper 1.程序包简介 2.下载安装 ...

  8. 基于Golang的监听读取配置文件的程序包开发——simpleConfig_v1

    基于Golang的监听&读取配置文件的程序包开发--simpleConfig_v1 [阅读时间:约10分钟] 一.配置文件概述 二.系统环境&项目介绍 1.系统环境 2.项目的任务要求 ...

  9. 基于Golang的CLI 命令行程序开发

    基于Golang的CLI 命令行程序开发 [阅读时间:约15分钟] 一. CLI 命令行程序概述 二. 系统环境&项目介绍&开发准备 1.系统环境 2.项目介绍 3.开发准备 三.具体 ...

最新文章

  1. ubuntu 系统设置bugzilla制
  2. Bagging和Boosting 概念及区别
  3. [luogu5004]专心OI - 跳房子【矩阵加速+动态规划】
  4. android 线程太多,应用程序可能在其主线程上做了太多的工作。
  5. 揭秘自编码器,一种捕捉数据最重要特征的神经网络(视频+代码)
  6. http://ju.outofmemory.cn/entry/307891---------TICK
  7. `object.__new__`应用
  8. java版b2b2c社交电商spring cloud分布式微服务-服务提供与调用
  9. 面料经纬向、正反面判别方法
  10. Atitit 学习的本质 团队管理与培训的本质 attilax总结 v2
  11. 深度学习图像分类(四): GoogLeNet(V1,V2,V3,V4)
  12. 微服务之核心架构思维
  13. Intent的设想与天马行空
  14. ICCV2021 | TOOD:任务对齐的单阶段目标检测
  15. 火遍全网的 ChatGPT,给你的求职新方向
  16. 端午节,我用 Python 画了一盘粽子送给大家
  17. 工具篇 之 iTerm 2 用户名修改(基于 iTerm 2 + oh-my-zsh)
  18. IDEA界面太丑了 我教你修改界面吧
  19. 公众号PHP模板修改,PHP 实现发送模板消息(微信公众号版)
  20. 算一下你来到这个世界多少天?

热门文章

  1. oracle 的基本命令(一)
  2. 一个端口扫描的小程序
  3. 电大管理英语4计算机期末考试,国开电大管理英语1单元自测4答案
  4. 为什么电脑不能打字_电脑不能打字怎么办?键盘没坏但无法打字的解决方法
  5. Python入门(02) -- 列表操作
  6. vue 组件 props配置
  7. Java通过引用操作对象的“共享”特性
  8. 分析maven依赖导入失败原因
  9. Java基础—集合2Set接口和Map接口
  10. 基于9款CSS3鼠标悬停相册预览特效