Skip to content

一些常见问题

整理常被问到的细节问题,像类似于并发、GC、GMP等,就单独开文章去写,其它的部分就更新在这里。

如何理解协程

首先我们要从操作系统的进程说起,虽然linux本身已经让进程创建的成本很低了,但实际上进程的创建的开销对于操作系统而言还是太大。
为此,又有了线程,线程是操作系统向其分配处理器的最小单元。同一进程下的多线程本身享有进程中的全部系统资源,但每个线程本身,还是有自己的调用栈,寄存器。协程的本质其实就是用户态线程。

其实又会延伸另外一个问题,多线程场景下,单个线程的崩溃是否可能引起其它线程的崩溃?

关于golang中的逃逸分析

逃逸分析,其实就是确认在哪里可以访问到指针,核心目的是将内存分配到堆上或者分配到栈上。 逃逸分析的基本原则

如果函数外部没有引用,则优先放到栈中; 如果函数外部存在引用,则必定放到堆中; 也会有一些异常情况:

  1. interface{} 动态类型逃逸
  2. 栈空间大小不足

更多细节可以参考着两篇文章:

  1. Go逃逸分析
  2. 逃逸分析是怎么进行的

slice和map的底层原理

可参考这篇文档 核心是理解其底层结构

go
type slice struct {
	array unsafe.Pointer // 元素指针
	len   int // 长度 
	cap   int // 容量
}

如何优雅退出协程

Golang中切片的扩展规则

其实可以理解为切片如何做自动扩容 可以参考这篇文章,以及golang中的源码

go
	newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
		const threshold = 256
		if old.cap < threshold {
			newcap = doublecap
		} else {
			// Check 0 < newcap to detect overflow
			// and prevent an infinite loop.
			for 0 < newcap && newcap < cap {
				// Transition from growing 2x for small slices
				// to growing 1.25x for large slices. This formula
				// gives a smooth-ish transition between the two.
				newcap += (newcap + 3*threshold) / 4
			}
			// Set newcap to the requested cap when
			// the newcap calculation overflowed.
			if newcap <= 0 {
				newcap = cap
			}
		}
	}

扩容的规则显而易见,新的cap的计算分为以下几种情况:
1)如果没有达到当前分配的容量,其实就不需要扩容,cap不变
2)newcap超过oldcap的2倍的话,直接扩容到新的cap
3)新的cap未超过旧的cap的2倍,但小于256,则直接newcap
4)如果已经超过256,则每次扩容1.25倍

由第一个问题可以引申出第二个问题,golang中的map的扩容

这块可以参考这篇文章

Golang中json解析相关的问题

go
	//测试下切片相关的操作
	var a []int
	if a == nil {
		fmt.Println("a is nil")
	}

	b := make([]int, 0)
	if b == nil {
		fmt.Println("b is nil")
	}
	//output
	// a is nil

这个是类型初始化方式的不同,golang中是否会分配默认值,或者是实际占用内存,类似于用var这种方式定义切片,其实本身是没有实际分配内存的 会导致json解析的结果不同

关于反射的使用场景

我理解其实线上服务应该尽可能的少用反射,因为反射本身是有性能损耗的,但一些场景来说,其实反射也是不可或缺的 关于反射的原理,可以参考这篇文章
网上的示例使用场景是用于类似于ORM框架这种,将对象本身转化成sql。其实golang源码中的json包也用了反射,将json反序列化到对象上

关于字符串的遍历

相关链接,自己之前也一直模糊的点在于如何遍历字符串,这个文章里面就说得比较清楚,range是用字符遍历,s[i]这种方式是用字节去遍历,就是所谓的rune类型,也是int32的别名