Skip to content

高级篇

for select时,如果通道已经关闭会怎么样?如果select中只有一个case呢?

  • select中如果任意某个通道有值可读时,它就会被执行,其他被忽略。
  • 如果没有default字句,select将有可能阻塞,直到某个通道有值可以运行,所以select里最好有一个default,否则将有一直阻塞的风险。
go
const fmat = "2006-01-02 15:03:04"
func main() {
	c := make(chan int)
	go func() {
		time.Sleep(1 * time.Second)
		c <- 10
		close(c)
	}()
	for {
		select {
		case x, ok := <-c:
			fmt.Printf("time=%v,x=%v,ok=%v \r\n", time.Now().Format(fmat), x, ok)
			time.Sleep(500 * time.Millisecond)
		default:
			fmt.Printf("time=%v,没有读到信息,进入default \r\n", time.Now().Format(fmat))
			time.Sleep(500 * time.Millisecond)
		}
	}
}

简单聊聊内存逃逸?

  1. 什么是内存逃逸:golang程序变量会携带有一组校验数据,用来证明它的整个生命周期是否在运行时完全可知。如果变量通过了这些校验,它就可以在栈上分配。否则就说它"逃逸"了,必须在堆上分配。
  2. 常见情况:
    1. 在方法内把局部变量指针返回:局部变量原本应该在栈中分配,在栈中回收。但是由于返回时被外部引用,因此其生命周期大于栈,则溢出。
    2. 发送指针或带有指针的值到 channel 中:在编译时,是没有办法知道哪个 goroutine 会在 channel 上接收数据。所以编译器没法知道变量什么时候才会被释放。
    3. 在一个切片上存储指针或带指针的值:一个典型的例子就是 []*string 。这会导致切片的内容逃逸。尽管其后面的数组可能是在栈上分配的,但其引用的值一定是在堆上。
    4. slice 的背后数组被重新分配了,因为 append 时可能会超出其容量( cap ):slice 初始化的地方在编译时是可以知道的,它最开始会在栈上分配。如果切片背后的存储要基于运行时的数据进行扩充,就会在堆上分配。
    5. 在 interface 类型上调用方法:在 interface 类型上调用方法都是动态调度的 —— 方法的真正实现只能在运行时知道。想像一个 io.Reader 类型的变量 r , 调用 r.Read(b) 会使得 r 的值和切片b 的背后存储都逃逸掉,所以会在堆上分配。
  3. 怎么避免内存逃逸?
    1. go build -gcflags=-m 查看逃逸情况

go垃圾回收机制

  • 引用计数算法
    • 引用计数通过在对象上增加自己被引用的次数,被其他对象引用时加1,引用自己的对象被回收时减1,引用数为0的对象即为可以被回收的对象,这种算法在内存比较紧张和实时性比较高的系统中使用比较广泛,如php,Python等。
  • 三色标记法
  • 三色标记 + 混合写屏障
    • 白色对象表示暂无对象引用的潜在垃圾,其内存可能会被垃圾收集器回收。
    • 灰色对象表示活跃的对象,黑色到白色的中间状态。
    • 黑色对象表示活跃的对象,包括不存在引用外部指针的对象以及从根对象可达的对象。
  1. 将所有对象标记为白色。
  2. 从根节点集合出发,将第一次遍历到的节点标记为灰色放入集合列表中。
  3. 遍历灰色集合,将灰色节点遍历到的白色节点标记为灰色,并把灰色节点标记为黑色。
  4. 循环这个过程。
  5. 直到灰色节点集合为空,回收所有的白色节点。