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程死锁,程序崩溃。
未完待续。。。