要么改变世界,要么适应世界

浅析Go中的make函数

2024-08-06 00:09:45
0
目录

go中,make函数一般用于创建切片、映射和管道,对这三种变量使用make将会被替换为:

  • runtime.makeslice:创建切片,然后返回切片结构体
  • runtime.makemap:创建映射表,然后返回映射表结构体
  • runtime.makechan:创建管道,然后返回管道结构体

下面我们来看看这几个函数内部的源码

makeslice

切片结构体runtime.slice定义为:

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

创建切片变量方法为:

sl := make([]int, 0, 10)

先来看源码的实现:

func makeslice(et *_type, len, cap int) unsafe.Pointer {
	mem, overflow := math.MulUintptr(et.Size_, uintptr(cap))
	if overflow || mem > maxAlloc || len < 0 || len > cap {
		// NOTE: Produce a 'len out of range' error instead of a
		// 'cap out of range' error when someone does make([]T, bignumber).
		// 'cap out of range' is true too, but since the cap is only being
		// supplied implicitly, saying len is clearer.
		// See golang.org/issue/4085.
		mem, overflow := math.MulUintptr(et.Size_, uintptr(len))
		if overflow || mem > maxAlloc || len < 0 {
			panicmakeslicelen()
		}
		panicmakeslicecap()
	}

	return mallocgc(mem, et, true)
}

函数比较简短,先是判断一下用户需要的申请容量是否是在允许的范围之内,并判断长度和容量是否是合法值,然后调用 mallocgc函数申请内存:

// Allocate an object of size bytes.
// Small objects are allocated from the per-P cache's free lists.
// Large objects (> 32 kB) are allocated straight from the heap.
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {}

而上述函数中,如果申请的空间大于32kb时,将会从堆上分配,否则从空闲列表中分配。

运行完make后,切片结构体中的array指针将会指向新申请的,而lensize分别设置为用户传递给make的值.

makemap

函数签名为:

func makemap(t *maptype, hint int, h *hmap) *hmap

第一个参数类型为maptype,它是abi.MapType别名,不同的类型所需的内存占用不同;第二个参数为预计元素的容量,第三个是hmap的指针,它是map的结构体:

// A header for a Go map.
type hmap struct {
	// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.
	// Make sure this stays in sync with the compiler's definition.
	// 表示hamp中的元素数量
    count     int // # live cells == size of map.  Must be first (used by len() builtin)
	// 用于表示hmap处于什么状态,例如 迭代器正在使用桶
    flags     uint8
	// hmap中的哈希桶的数量为 2^B
    B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
	// hmap中溢出桶的大致数量
    noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
	// 哈希种子,在hmap被创建时指定,用于计算哈希值
    hash0     uint32 // hash seed
	// 存放哈希桶数组的指针
	buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
	// 存放hmap在扩容前哈希桶数组的指针。
    oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
	// 存放着hmap中的溢出桶
    nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)

	extra *mapextra // optional fields
}

回到makemap函数

// makemap implements Go map creation for make(map[k]v, hint).
// If the compiler has determined that the map or the first bucket
// can be created on the stack, h and/or bucket may be non-nil.
// If h != nil, the map can be created directly in h.
// If h.buckets != nil, bucket pointed to can be used as the first bucket.
func makemap(t *maptype, hint int, h *hmap) *hmap {
	mem, overflow := math.MulUintptr(uintptr(hint), t.Bucket.Size_)
	if overflow || mem > maxAlloc {
		hint = 0
	}

	// initialize Hmap
	if h == nil {
		h = new(hmap)
	}
	h.hash0 = fastrand()

	// Find the size parameter B which will hold the requested # of elements.
	// For hint < 0 overLoadFactor returns false since hint < bucketCnt.
	B := uint8(0)
	for overLoadFactor(hint, B) {
		B++
	}
	h.B = B

	// allocate initial hash table
	// if B == 0, the buckets field is allocated lazily later (in mapassign)
	// If hint is large zeroing this memory could take a while.
	if h.B != 0 {
		var nextOverflow *bmap
		h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
		if nextOverflow != nil {
			h.extra = new(mapextra)
			h.extra.nextOverflow = nextOverflow
		}
	}

	return h
}

首先就是计算预计分配的内存是否会超出最大分配内存(第7到10行),如果数值溢出或者超出了最大分配内存,则设置hint=0,后续需要用它来计算桶数组的容量。

接着判断传进来的h指针是否为空,为空则申请一个大小为maptype的空间,然后计算出一个随机的哈希种子(第13到16行)

然后以迭代的方式找到一个符合一定条件的B(第20-23行),它指定了哈希桶的个数。

接着就为哈希桶分配内存(第29-36行)

最后,返回这个指向hmap的指针。

makechan

按照上面的案例,我们先来看一下在运行时与channel息息相关的runtime.hchan结构体 :

type hchan struct {
    // 对应于 len(hchan)
	qcount   uint           // total data in the queue
    // 对应于 cap(hchan)
    dataqsiz uint           // size of the circular queue
	buf      unsafe.Pointer // points to an array of dataqsiz elements
	elemsize uint16
	closed   uint32
	elemtype *_type // element type
	sendx    uint   // send index
	recvx    uint   // receive index
	recvq    waitq  // list of recv waiters
	sendq    waitq  // list of send waiters

	lock mutex
}

它实际上是一个有锁的同步环形队列.

创建管道:

ch := make(chan int, size)

实际上是调用makechan

func makechan(t *chantype, size int) *hchan {
	elem := t.Elem

	// compiler checks this but be safe.
	if elem.Size_ >= 1<<16 {
		throw("makechan: invalid channel element type")
	}
	if hchanSize%maxAlign != 0 || elem.Align_ > maxAlign {
		throw("makechan: bad alignment")
	}

	mem, overflow := math.MulUintptr(elem.Size_, uintptr(size))
	if overflow || mem > maxAlloc-hchanSize || size < 0 {
		panic(plainError("makechan: size out of range"))
	}

	// Hchan does not contain pointers interesting for GC when elements stored in buf do not contain pointers.
	// buf points into the same allocation, elemtype is persistent.
	// SudoG's are referenced from their owning thread so they can't be collected.
	// TODO(dvyukov,rlh): Rethink when collector can move allocated objects.
	var c *hchan
	switch {
	case mem == 0:
		// Queue or element size is zero.
		c = (*hchan)(mallocgc(hchanSize, nil, true))
		// Race detector uses this location for synchronization.
		c.buf = c.raceaddr()
	case elem.PtrBytes == 0:
		// Elements do not contain pointers.
		// Allocate hchan and buf in one call.
		c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
		c.buf = add(unsafe.Pointer(c), hchanSize)
	default:
		// Elements contain pointers.
		c = new(hchan)
		c.buf = mallocgc(mem, elem, true)
	}

	c.elemsize = uint16(elem.Size_)
	c.elemtype = elem
	c.dataqsiz = uint(size)
	lockInit(&c.lock, lockRankHchan)

	if debugChan {
		print("makechan: chan=", c, "; elemsize=", elem.Size_, "; dataqsiz=", size, "\n")
	}
	return c
}

上面的函数先检查一下参数,然后根据传入的size和元素类型elem.size来计算预计需要的内存大小,进而给管道分配内存,可以分为三种情况:

  1. mem == 0,即size=0(因为elem.Size_不可能为零),这时候队列大小为零,只会分配hchanSize,该大小实际上是hchan结构体大小加上一定的开销。
  2. 管道类型不包含指针,则会分配hchanSize+对应类型占用的大小的空间。
  3. 管道类型包含指针,管道和环形队列的内存单独分配

然后,把相关的参数初始化(第39行到41行)

最后通过调用 lockInit(&c.lock, lockRankHchan),确保了新创建的通道有一个初始化好的互斥锁,可以安全地用于并发环境中的数据交换。

历史评论
开始评论