Go学习中的问题及解决方法

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,若有多个条件同时满足,将随机执行某一个caseselect语句就执行一次,这也是为何要配合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
但是如果mapvalueint,是可以修改的,因为修改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的数据类型相同,keyvalue的数据类型可以不同。
map中的key是唯一的,且需要支持==!=操作,常用类型:intrunestring,结构体(每个元素都需要支持==!=操作),指针,以及基于这些类型自定义的类型。float32/64一般不作为key的类型使用。

问题描述:

在使用for range遍历map时,对map中的keyvalue取地址打印,输出结果是同一块地址(keyvalue对应地址不同),也就是说,无法通过取得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的地址时,ElementnextprevValue的地址没有联系。这是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.nextElement.prev有关系,但在上述代码中前者使用fmt.Println()打印的是Element.Value的地址,而后者使用fmt.Printf()打印的是对应Element的地址。
当然,前者不能通过fmt.Println("value: ", i.Value, " address: ", &i, " element: ", i),打印Element的地址,其实打印的是临时变量i的地址;但是利用fmt.Printf()却可以正常打印,这里有点懵逼。不管怎么样,打印变量地址时还是规矩按fmt.Printf()输出吧。
总结

  • list.Elementnextprev指向的都是list.Element的地址,而不是list.Element.Value的地址,所以ElementValue的地址不同是正常的。
  • 在打印变量地址时,规规矩矩使用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程死锁,程序崩溃。

未完待续。。。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值