Golang学习中的问题及解决方法
个人学习总结,鉴于个人水平有限,对某些问题的思考有所不足,仅供参考。
问题1:for循环中启动多个go协程
问题描述:
在一个for循环中启动多个go协程,但运行时,只有最后一个(也不是最后一个,这个应该是参数传递的问题)在运行。
代码示例:
package main
import (
"fmt"
"time"
)
func main() {
go_num := 5
for i := 0; i < go_num; i++ {
go func() {
fmt.Printf("This is go %d\n", i)
}
}
time.Sleep(time.Millisecond)
}
输出结果如下:
This is go 5
This is go 5
This is go 5
This is go 5
This is go 5
个人理解函数的执行顺序是,for循环执行自变量i的递增,同时启动多个go程执行其中的匿名函数(实际上并没有立即执行);在自变量i递增结束,i的值为5;在循环退出时遇到time.Sleep时执行循环体中启动的go程(闭包go协程里面引用的是变量i的地址),打印输出结果。补充一句:具体输出结果依赖go协程的执行顺序,如果协程在循环体中执行了,输出结果可能不同。
解决方法:
- 通过参数传递数据到协程,如:
for i := 0; i < go_num; i++ {
go func(num int) {
fmt.Printf("This is go %d\n", num)
}(i)
}
- 在
for循环中加一个临时变量tmp,每次将i的值赋值给tmp,然后tmp可直接传递给协程
此方法可以解决不能通过参数传递数据的情况(某些第三方库不能传参数)
for i := 0; i < go_num; i++ {
tmp := i
go func() {
fmt.Printf("This is go %d\n", tmp)
}()
}
顺便提一句:输出结果的打印顺序不一定是按循环的顺序。
问题2:杀死for“死循环”(channel方式)
问题描述:
在go协程中通过一个for死循环不断从id分配器中获取id,直到channel传递值杀死for死循环,进而退出go协程,在打印id最大值后退出主线程main。实际的代码(错误的)运行时,一直卡死在for循环中,无法通过传递channel值select合适的case退出for死循环。
代码示例:
package main
import (
"IdAllocator" // IdAllocator defines functions like Allocate_id()(used to allocate id) etc.
"fmt"
"time"
)
func GoIdAllocator(ids *IdAllocator.Idallocator, go_num int) {
for {
tmp := ids.Allocate_id()
fmt.Printf("Go %d: Ids's type: %T, value: %d\n", go_num, tmp, tmp)
}
}
func main() {
var ids IdAllocator.Idallocator
str := "ids_test"
ids.Init(str, 0) // initialize ids, str is key, 0 set max_id = 1
go_num := 10
done := make(chan int)
defer close(done)
for i := 0; i < go_num; i++ {
go func(num int) {
select {
case <-done:
fmt.Printf("Go %d done\n", num)
return
default:
GoIdAllocator(&ids, num)
}
}
}(i)
}
time.Sleep(3 * time.Millisecond)
for i := 0; i < go_num; i++ {
done <- 1
}
fmt.Printf("The key: %s\n", ids.Get_key())
fmt.Printf("The max_id: %d\n", ids.Get_max_id())
}
温馨提示:由于死循环无法结束,在ubuntu终端下使用Ctrl + C来终止程序。
终端下执行go run program_name.go > result.log,发现生成的log文件很大,而且无法在文件中搜索到关键字max_id或者done等,说明通过channel传递值并没有被select到。
再来浏览代码,发现定义的GoIdAllocator函数只是一个死循环,在select中,当第一次没有通道值传入进来时,就开始执行default语句,于是卡死在死循环中,无法退出。所以,根本原因是:正常退出死循环需要在死循环中设置结束条件,而不是在循环外部。
解决方法:
package main
import (
"IdAllocator" // IdAllocator defines functions like Allocate_id()(used to allocate id) etc.
"fmt"
"time"
)
func main() {
var ids IdAllocator.Idallocator
str := "ids_test"
ids.Init(str, 0)
go_num := 10
done := make(chan int)
defer close(done)
for i := 0; i < go_num; i++ {
go func(num int) {
for {
select {
case <-done:
fmt.Printf("Go %d done\n", num)
return
default:
tmp := ids.Allocate_id()
fmt.Printf("Go %d: Ids's type: %T, value: %d\n", num, tmp, tmp)
}
}
}(i)
}
time.Sleep(3 * time.Millisecond)
for i := 0; i < go_num; i++ {
done <- 1
}
fmt.Printf("The key: %s\n", ids.Get_key())
fmt.Printf("The max_id: %d\n", ids.Get_max_id())
}
执行上述代码,go run program_name.go > result.log,打印结果到result.log中,在该文件中可以找到打印出的关键字max_id或者done;而且,打印max_id在最后执行,得到不断取id后的最大值。从代码中可以看出,在go协程中启动一个for“死循环”(其实非严格意义上的死循环,因为循环内部有结束循环的控制条件),select接收通道传递的值,收到则退出循环,退出go协程;否则将执行default语句,不断获取并打印id值。
总结:
- 杀死形如
for {}的死循环,需要在循环内部设置循环退出条件,在循环外部设置的条件无法退出“死循环”。 - 配合使用
for {}和select {},通过在select内部设置循环结束条件来结束“死循环”,select语句将执行满足条件的case,若有多个条件同时满足,将随机执行某一个case。select语句就执行一次,这也是为何要配合for {}使用的原因,每次循环都会执行select来判断该执行哪个case。 - 注意
defer语句的使用。这里,打印max_id值不能再go协程前面使用defer语句打印。虽然defer会推迟到主线程退出时执行打印,但是max_id值是获取到的当下值,而不是获取max_id的地址。导致打印出的max_id仍然是go协程执行前的值。
问题3:杀死for“死循环”(time.After())
问题描述:
在go协程中,配合使用for{ select {...} }和定时器time.After()运行一段时间后终止“死循环”,在case中直接接收time.After()值无法退出循环。
代码示例:
package main
import (
"IdAllocator" // IdAllocator defines functions like Allocate_id()(used to allocate id) etc.
"fmt"
"time"
)
func main() {
var ids IdAllocator.Idallocator
str := "ids_test"
ids.Init(str, 0)
go_num := 1
for i := 0; i < go_num; i++ {
go func(timeout time.Duration) {
for {
select {
case <-time.After(timeout):
fmt.Printf("Timeout.")
return
default:
tmp := ids.Allocate_id()
fmt.Printf("Ids's type: %T, value: %d\n", tmp, tmp)
}
}
}(10 * time.Millisecond)
}
time.Sleep(time.Second) // wait a second for goroutines excuting
fmt.Printf("The key: %s\n", ids.Get_key())
fmt.Printf("The max_id: %d\n", ids.Get_max_id())
}
执行go run program_name.go > result.log,输出结果到log文件中,在该文件中搜索不到关键字Timeout,这说明go协程并没有执行到select中的case语句。直接输出结果到终端,发现程序一直运行打印id值1秒后退出,这说明go协程是在主线程退出后被迫退出的。以上结果说明time.After(timeout)值没有传入。
进一步分析,直接传入time.After(timeout)导致每次执行select语句时,其值都会被更新,也就是说每次都等待10毫秒后执行,这样永远不会执行到相应的case语句,从而退出循环。
解决方法:
package main
import (
"IdAllocator"
"fmt"
"time"
)
func main() {
var ids IdAllocator.Idallocator
str := "ids_test"
ids.Init(str, 0)
go_num := 1
for i := 0; i < go_num; i++ {
go func(timeout time.Duration) {
dd := time.After(timeout) // use temporary variable out of for loop
for {
select {
case <-dd:
fmt.Printf("Timeout.\n")
return
default:
tmp := ids.Allocate_id()
fmt.Printf("Ids's type: %T, value: %d\n", tmp, tmp)
}
}
}(10 * time.Millisecond)
}
time.Sleep(time.Second)
fmt.Printf("The key: %s\n", ids.Get_key())
fmt.Printf("The max_id: %d\n", ids.Get_max_id())
}
运行上述代码,发现正确打印Timeout.并等待一段时间后,打印key和max_id,程序终止。这说明go协程在主线程退出前已经退出了。
总结:
- 在退出形如
for {}的“死循环”时,利用time.After()退出循环,不能将它置于循环内部,除非需要利用不断更新的值来退出循环体。 - 使用临时变量置于“死循环”外部。
问题4:关于map的原地修改
问题描述:
在定义一结构体后,将该结构体map到字符串上,无法原地修改对应key值中的value。
代码示例:
package main
type Student struct {
Name string
Id int
}
func main() {
s := make(map[string]Student)
s["CaiJi"] = Student{
Name: "zeze",
Id:111,
}
s["CaiJi"].Id = 250 // it can't be compiled
}
上面的代码会编译失败,因为在go中 map中的赋值属于值copy,就是在赋值的时候是把Student的完全复制了一份,复制给了map。而在go语言中,是不允许将其修改的。如果不是原地修改的话,可以直接将相应的值更新,相当于构造新的值,复制给map中对应的key。
但是如果map的value为int,是可以修改的,因为修改map中的int属于赋值的操作。如:
package main
type Student struct {
Name string
Id int
}
func main() {
s1 := make(map[string]int)
s1["CaiJi"] = 2
s1["CaiJi"] = 3
}
解决方法:
在Go中原地修改map中的value,传指针!当结构体较大时,指针效率更好,不需要值copy。当然,如果map中的value为*int指针类型,那么赋值时不可以用&123,因为int为常量,不占内存,没有内存地址。所以,可以给一个int变量赋值,然后传入该变量的地址。
package main
import "fmt"
type Student struct {
Name string
Id int
}
func main() {
s2 := make(map[string]*int)
n := 1
s2["CaiJi"] = &n
fmt.Println(s2)
}
问题5:map遍历的坑
map类型写为:map[key]value,要求所有的key的数据类型相同,所有value的数据类型相同,key和value的数据类型可以不同。
map中的key是唯一的,且需要支持==或!=操作,常用类型:int,rune,string,结构体(每个元素都需要支持==或!=操作),指针,以及基于这些类型自定义的类型。float32/64一般不作为key的类型使用。
问题描述:
在使用for range遍历map时,对map中的key和value取地址打印,输出结果是同一块地址(key和value对应地址不同),也就是说,无法通过取得map中键值对的地址来遍历输出键值对结果。
代码示例:
package main
import "fmt"
func main() {
// the output of value's address in map is random, even if you use a variable ch(its' address not change), it doesn't affect the output.
m := make(map[string]int)
// there is no char type in Go, you can use []byte instead.
// byte type is align-name of uint8, so it can be operated with integer, like 10 * 'a'
// but 10 * ch is not as equal as 10 * 'a'
// note that constant can do the above operation, not variable.
var ch byte
ch = 'a'
// another problem remains to solve.
// use constant int or constant byte to do operations like multiply, if both are constant, the type of result will be int(in fact, byte is int8).
// But if one is constant, the other is variable(another type), when do multipling, the constant will be transformed to the same type of variable firstly, then do operation.
// use different type to multiply, you should tranform data type explicitly.
// if you use constant
var num int = 10
fmt.Println(10 * ch)
fmt.Println(num * 'a')
fmt.Printf("%c\n", 10 * ch)
fmt.Printf("%c\n", num * 'a')
fmt.Printf("%c\n", byte(num) * ch)
fmt.Printf("%c\n", num * int(ch))
for i := 1; i <= 7; i++ {
// ++ is not an expression, it only can be used as a statement in Go, eg. you can't use it like m[string(ch++)].
// ++ operator can be used by byte or int type, but not string.
m[string(ch)] = i
ch++
}
var bs []*int
for k, v := range m {
// according to output, k uses the same address, so does v.
fmt.Printf("k:[%p].v:[%p]\n", &k, &v)
// get the address of v
bs = append(bs, &v)
}
// output
for _, b := range bs {
// the output are all the same one(in range of m's value)
fmt.Println(*b)
}
}
由于map遍历的顺序是随机的,使用for range遍历的时候,k使用的是同一块内存,v也使用同一块内存,同时,这块地址是临时分配的。虽然v的地址没有变化,但其内容一致变化,当遍历完成时,v的内容是map遍历时最后遍历的元素的值。当程序将v的地址放入到slice中的时候,slice不断地插入v的地址,由于v一直都是那块地址,因此slice中的每个元素记录的都是v的地址。因此,当打印slice中的内容的时候,都是同一个值。
解决方法:
遍历map中的元素时,想输出元素值,直接打印相应的值即可,不要对元素取地址,也不要通过对元素地址的操作来更改对应的值。
for k, v := range m {
fmt.Printf("k:[%v].v:[%v]\n", k, v) // output key and value directly
}
问题6:map-list的配合使用
使用map快速查找定位元素,使用list进行元素的排序,移动等操作。
问题描述
使用Go语言官方提供的list,需要解决list.Element和用户自定义的数据类型的挂钩。另外,还要注意new()函数的使用方法,特别是通过map快速定位元素后,使用指针在list中移动该元素时,不能使用new(list.Element)生成新的元素(相当于生成了个新的变量)。
代码示例
package main
import (
)
待补充
问题7:打印数据地址
问题描述
打印自定义结构体的数据地址时,使用fmt.Printf()打印,而不要使用fmt.Println()自己取地址后打印,在使用list时会出现问题。
代码示例
package main
import (
"container/list"
"fmt"
)
func testList() {
l := list.New()
var str = []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"}
for i := 0; i < 5; i++ {
l.PushBack(str[i])
}
for i := l.Front(); i != nil; i = i.Next() {
fmt.Println("value: ", i.Value, " address: ", &i.Value, " element: ", i)
}
}
func main() {
testList()
}
由输出结果可以看出,使用fmt.Println()打印list中的Element.Value的地址时,Element的next和prev和Value的地址没有联系。这是fmt.Println()的打印有关,具体什么原因尚未探讨。解决方法是使用fmt.Printf(),设定具体的输出格式来打印结果。
解决方法
package main
import (
"container/list"
"fmt"
)
func testList() {
l := list.New()
var str = []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"}
for i := 0; i < 5; i++ {
l.PushBack(str[i])
}
for i := l.Front(); i != nil; i = i.Next() {
fmt.Printf("value: %s, address: %p, element: %v \n", i.Value, i, i)
}
}
func main() {
testList()
}
进一步分析,我发现自己偷换了概念,其实我想查看的是Element.Value的地址是否与Element.next和Element.prev有关系,但在上述代码中前者使用fmt.Println()打印的是Element.Value的地址,而后者使用fmt.Printf()打印的是对应Element的地址。
当然,前者不能通过fmt.Println("value: ", i.Value, " address: ", &i, " element: ", i),打印Element的地址,其实打印的是临时变量i的地址;但是利用fmt.Printf()却可以正常打印,这里有点懵逼。不管怎么样,打印变量地址时还是规矩按fmt.Printf()输出吧。
总结:
list.Element的next和prev指向的都是list.Element的地址,而不是list.Element.Value的地址,所以Element和Value的地址不同是正常的。- 在打印变量地址时,规规矩矩使用
fmt.Printf()并指定%p为打印格式即可,使用fmt.Println()可能得不到预想的结果,特别是在循环体中使用临时变量(尽管该临时变量指向了想要利用的变量)时。奇怪的是,为何fmt.Printf()就可以得到预想的结果呢(尚待探讨)?
问题8:关于接口的实现的坑
代码示例
package main
import "fmt"
type Subject interface {
Exam(int) string
}
type Math struct{}
func (ma *Math) Exam(score int) (result string) {
if score >= 60 {
result = "pass"
} else {
result = "not pass"
}
return
}
func main() {
var tmp Subject = Math{}
fmt.Println(tmp.Exam(60))
}
上述代码编译报错,指出Math没有实现Exam接口。因为是用*Math实现的,在main函数中如果使用var声明变量,则需要用var tmp = &Math{},否则应使用tmp := Math{}赋值。但不同的声明方式具体有什么区别,我还没搞懂。
解决方法
package main
import "fmt"
type Subject interface {
Exam(int) string
}
type Math struct{}
func (ma *Math) Exam(score int) (result string) {
if score >= 60 {
result = "pass"
} else {
result = "not pass"
}
return
}
func main() {
tmp := Math{}
// or, you can also use 'var tmp Subject = &Math{}' to declare a variable '&Math{}'
// it also can be 'var tmp = &Math{}' or 'tmp := &Math{}'
fmt.Println(tmp.Exam(60))
}
问题9:切片和垃圾回收
代码示例
var digitRegexp = regexp.MustCompile("[0-9]+")
func FindDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
return digitRegexp.Find(b)
}
函数 FindDigits 将一个文件加载到内存,然后搜索其中所有的数字并返回一个切片。切片的底层指向一个数组,该数组的实际容量可能要大于切片所定义的容量。只有在没有任何切片指向的时候,底层的数组内存才会被释放,这种特性有时会导致程序占用多余的内存。这段代码可以顺利运行,但返回的 []byte 指向的底层是整个文件的数据。只要该返回的切片不被释放,垃圾回收器就不能释放整个文件所占用的内存。换句话说,一点点有用的数据却占用了整个文件的内存。
想要避免这个问题,可以通过拷贝我们需要的部分到一个新的切片中。事实上,上面代码只能找到第一个匹配正则表达式的数字串。要想找到所有的数字,可以尝试下面这段代码:
解决方法
func FindFileDigits(filename string) []byte {
fileBytes, _ := ioutil.ReadFile(filename)
b := digitRegexp.FindAll(fileBytes, len(fileBytes))
c := make([]byte, 0)
for _, bytes := range b {
c = append(c, bytes...)
}
return c
}
问题10:defer语句打印信息
代码示例
func PrintThings() (idx int) {
defer fmt.Printf("print idx by defer statement. idx = %d\n", idx)
idx = 7
return
}
func main() {
PrintThings()
}
执行上面的代码,打印出的idx值为0,不是7。这是因为defer代码块会在函数调用链表中增加一个函数调用。这个函数调用不是普通的函数调用,而是会在函数正常返回,也就是return之前添加一个函数调用。但是直接使用打印语句相当于在return之前调用打印语句,该打印语句中的参数是前面就被解析传递进去的,即使后面修改了相应的变量,传入的参数也不会修改。
解决方法
func PrintThings() (idx int) {
defer func() { fmt.Printf("print idx by defer statement. idx = %d\n", idx) }()
idx = 7
return
}
func main() {
PrintThings()
}
解决上面问题的方法是,使用匿名函数(闭包函数)。因为这相当于在return之前调用一个真正的函数,函数中使用的参数是在return之前修改后的变量,在该函数中可以对命名返回值进行相关操作,可以打印相关信息。defer语句一般适用于命名返回值的处理,对参数是指针变量的情况,根据具体情况具体分析。下面是defer使用的原则:
- defer适用于命名返回值的处理
- 当defer被声明时,其参数就会被实时解析
- defer执行顺序为先进后出
- defer可以读取有名返回值
问题11:无缓冲的channel的数据处理
问题描述
使用IDE开发工具Goland,直接Run如下代码,或者在终端使用go run codename.go,或者先go build codename.go,再执行可执行文件,都可能不会处理最后一条数据就直接退出。
代码示例
package main
import (
"fmt"
"time"
)
func main() {
var ch = make(chan string)
go func() {
for m := range ch {
time.Sleep(1 * time.Second) // simulate: it will cost a long time that receiver handle what sender send.
fmt.Println("Processed: ", m)
}
}()
ch <- "cmd.1"
ch <- "cmd.2"
ch <- "cmd.3"
ch <- "cmd.4"
ch <- "cmd.5"
}
解决方法
对无缓冲的channel而言,如果receiver准备好了,sender发送的数据会被立即处理,但是如果处理时间较长,sender会阻塞等待receiver接收下一个数据。那么,当sender发送完最后一个数据后,主函数继续向后执行,而go程中的receiver处理数据。此时,就有可能会出现主函数退出而go程还未来得及将最后一个数据处理完的情况。
package main
import (
"fmt"
"time"
)
func main() {
var ch = make(chan string)
var done = make(chan bool)
go func() {
for m := range ch {
time.Sleep(1 * time.Second) // simulate: it will cost a long time that receiver handle what sender send.
fmt.Println("Processed: ", m)
}
done <- true
}()
ch <- "cmd.1"
ch <- "cmd.2"
ch <- "cmd.3"
ch <- "cmd.4"
ch <- "cmd.5"
close(ch) // you have to close channel ch. Otherwise, it will cause deadlock, because goroutine is blocked in for-range(wait to receive data from ch).
<-done
}
可以使用另一个channel使主函数阻塞等待go程退出,但是必须注意关闭前面的无缓冲channel。否则,go程将阻塞在for-range处等待接收无缓冲channel发来数据,就会造成go程死锁,程序崩溃。
未完待续。。。