1 goroutine

1.1 论同步的重要性

详见官网

Goroutine对于内存的操作顺序需要同步。

对于单一goroutine,对于内存的读写是和程序代码顺序一致的。例如:代码中先写varA,再写varB,那么总是能保证varA先于varB被改变。

然而,对于多个goroutine,它们对于同一片内存的访问并不一定是按照代码的顺序进行的。不同goroutine对同一个资源的访问,如果不进行同步,往往会导致错误的结果。

Go的文档里有说:

A read r may observe the value written by a write w that happens concurrently with r. Even if this occurs, it does not imply that reads happening after r will observe writes that happened before w.

也就是说,可能会有以下的情况发生:

  +----------+                  +-----------+
  |    gr1   |                  |     gr2   |
  +-----+----+                  +-----+-----+
		|                             |
		|write varA                   |
		|                             |
		|write varB                   |
		|                             |
		|                             | read varB (changed)
		|                             |
		|                             | read varA (NOT CHANGED!!!)
		|                             |

这么一来,类似下面的代码也就不正确了:

type T struct {
	msg string
}

var g *T

func setup() {
	t := new(T)
	t.msg = "hello, world"
	g = t
}

func main() {
	go setup()
	for g == nil {
	}
	print(g.msg)
}

main()中不能保证总是输出hello, world.

解决的办法是使用同步原语,包括 chansync中的锁。

1.2 使用select的时候需要考虑竞争条件

详见官方博客

考虑有一个处理程序,它的职责是处理从各个goroutine输入的请求中的第一个,丢掉其他的请求(例如:从多个数据库读某个记录,仅处理第一个返回的结果)。程序一般会写成像下面这样的模式:

func Query(conns []Conn, query string) Result {
    ch := make(chan Result)
    for _, conn := range conns {
        go func(c Conn) {
            select {
            case ch <- c.DoQuery(query):
            default:
            }
        }(conn)
    }
    return <-ch
}

注意,这里新执行的goroutine都是非block形式的,也即如果当前channel满了或者没有receiver,则执行到default分支。

上面的代码有一个潜在的问题:如果主函数中的return <-ch 执行前,所有goroutine都已经结束了的话,那么主函数会一直卡在那里,从而报运行时错误。

一个类似的必现例子如下:

func foo() int {
	ch := make(chan int)
	go func() {
		select {
		case ch <- 1:
		default:
		}
	}()
	time.Sleep(time.Second)
	return <-ch
}

解决方法:不使用no buffer的channel,而是将channel的buffer数设置为需要接收的请求个数,这样甚至连default分支都不需要了。

2 接口

2.1 接口的receiver

接口可以被任何非接口的类型实现,包括结构体类型,基本类型(e.g. type T int),甚至函数类型(e.g. type T func()).

例如:

package animal

type Animal interface {
	Speak(what string) (out string)
}

// struct implementing interface
type Dog struct {
	accent string
}

func (dog *Dog) Speak(what string) (out string) {
	return (what + dog.accent)
}

// function implement interface
type Cat func() string

func CatFunc() string {
	return " Miewww..."
}

func (cat *Cat) Speak(what string) (out string) {
	return (what + (*cat)())
}

// primary type implement interface
type Cow string

func (cow *Cow) Speak(what string) (out string) {
	return (what + string(*cow))
}

// constructors
func NewDog() *Dog {
	return &Dog{" Awuuu..."}
}

func NewCat() *Cat {
	cat := Cat(CatFunc)
	return &cat
}

func NewCow() *Cow {
	cow := Cow(" Mowww...")
	return &cow
}

注意,上面接口实现中传递的是指针。在Go语言中有这么一条规则:

如果接口receiver声明的是指针类型,那么它只能被指针类型调用;如果接口receiver声明的是值类型,那么它可以被值类型调用,也可以被指针类型调用。

这条规则的后面部分能工作的原因是因为Go会在需要用到值的地方,自动将指针解引用获得对应的值,再继续(调用)。

这条规则的前面部分不接受以值类型调用,是因为以指针作为receiver的method可以修改指针指向的对象,如果传入的是个值(值传递会创建一个临时变量),那么如果Go自动获得这个值的地址,并对其做修改,这些修改只会影响这个临时变量,而不是method的调用者(如果传入的是类似于C++的引用,说不定就没这个限制了)。Go为了避免这种错误,所以做了这么一个限制。

但是,这条规则后面又针对指针类型的receiver提到:

当以值类型去调用指针类型的接口,如果值是可寻址的,那么Go会自动在获得它的地址之后再去调用。

(详见:这里)

2.2 接口定义

一般定义一个接口的时候,开发者会在包中定义一个receiver(例如,结构体),然后通常会提供一个NewXXX函数来返回这个receiver的指针,供用户调用(例如上面的NewDog())。

3 Debug

3.1 Goland 调试多个go routine

假设有以下代码:

package main 
             
import (     
    "fmt"    
    "strings"
)            
             
func main() {
    sendCh := make(chan string)
    recvCh := make(chan string)
             
    launchRoutine(recvCh, sendCh) // A
    sendCh <- "hello, world!"
    fmt.Println(<-recvCh)
}            
             
func launchRoutine(sendCh, recvCh chan string) {
    go func() {
        for {
			s := strings.ToUpper(<-recvCh)
            sendCh <- s // B
        }    
    }()      
}            

在Goland里同时在A和B点设置断点,然后开始运行。程序首先停止在A点,如果这时候使用Step Over,那debugger仅会在当前的goroutine中继续执行,而忽略其他goroutine中的断点。如果你想同时在其他goroutine中也能够停止,那么使用Run或者Run to Cursor.