Go语言中的make和new

关于Go语言中make和new的区别,已经在网上看到了很多文档,但是总觉得缺点什么,所以今天就自己写一篇文章来讲一下。

首先先说下网上说的关于make和new的区别,大致有以下几点:

  • make只能用于slice、map、channel的初始化,返回的是这三个类型本身。
  • new用于为任何类型分配内存,并返回指向该类型的指针。
  • new和make都能用于分配内存,但是make会初始化内存,而new不会。

make

make用于创建slice、map、channel,返回的是这三个类型本身。

有一个很普遍的说法:slice、map、channel是引用类型。也有人说,go语言中没有引用类型 。我们今天不争论这个,因为这只是概念上的问题,争论这个意义不大。

不过,Go官方的代码提交记录中,的确是在2013年就删除了对引用类型的支持。 而之所以能很多人还在说slice、map、channel、function、interface是引用类型,是因为这几种类型的实现中,有指针指向底层数据,比如slice, 在slice的实现中,有指针指向底层数组:

go 复制代码
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

如果用这个标准来判断,那么string也是引用类型,因为string的实现中,也有指针指向底层数据:

go 复制代码
type StringHeader struct {
	Data uintptr
	Len  int
}

slice, map, channel的区别

来看一个有趣的例子:

go 复制代码
package main

import (
	"fmt"
	"unsafe"
)

func main() {
	arr := make([]int, 4, 8)
	maps := make(map[string]string, 10)
	chans := make(chan int, 10)

	arr[0] = 1
	arr[1] = 2

	maps["a"] = "a"

	chans <- 1

	fmt.Println("sizeof slice:", unsafe.Sizeof(arr))
	fmt.Println("sizeof map:", unsafe.Sizeof(maps))
	fmt.Println("sizeof chan:", unsafe.Sizeof(chans))
}

运行结果: 可以看到,slice的大小是24字节,这个很好理解,因为slice的实现中,有指针指向底层数组,所以slice的大小是指针的大小(8字节)+ len(8字节)+ cap(8字节)。 而为什么map和chan的大小是8字节呢?是因为其底层结构体的大小都是8字节? 不是,map的底层对应的结构体是hmap,而chan的底层对应的结构体是hchan,其大小都远远超过了8字节:
map的底层结构体hmap

go 复制代码
// 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.
	count     int // # live cells == size of map.  Must be first (used by len() builtin)
	flags     uint8
	B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
	noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
	hash0     uint32 // hash seed

	buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
	oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
	nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)

	extra *mapextra // optional fields
}

chan的底层结构体hchan

go 复制代码
type hchan struct {
	qcount   uint           // total data in the queue
	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 protects all fields in hchan, as well as several
	// fields in sudogs blocked on this channel.
	//
	// Do not change another G's status while holding this lock
	// (in particular, do not ready a G), as this can deadlock
	// with stack shrinking.
	lock mutex
}

那为什么make出来的chan和map是8字节呢?而8字节,正好是指针的大小。 我们来看一下示例代码的汇编:

bash 复制代码
go build -gcflags -S ./main.go

运行结果: 可以看到,使用make创建map和chan时,分别调用了runtime.makemap和runtime.makechan,这两个函数的返回值都是指针,所以make出来的map和chan的大小都是8字节。
runtime.makemap

go 复制代码
// 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
}

runtime.makechan

go 复制代码
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.ptrdata == 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
}

slice和map用作函数参数时的区别

下面来看两个小例子:
slice作为函数参数

go 复制代码
package slicedemo

import (
	"fmt"
	"testing"
)

func changeSlice0(arr []int) {
	arr[0] = 100
	fmt.Printf("changeSlice0 length: %d, cap: %d\n", len(arr), cap(arr))
}

func changeSlice1(arr []int) {
	arr = append(arr, 101, 102, 103, 104, 105, 106, 107, 108)
}

func TestSlice0(t *testing.T) {
	arr := make([]int, 5, 10)
	arr[0] = 1
	arr[1] = 2
	arr[2] = 3
	arr[3] = 4
	arr[4] = 5

	fmt.Printf("TestSlice1 length: %d, cap: %d\n", len(arr), cap(arr))
	changeSlice0(arr[1:])
	fmt.Printf("arr: %v\n", arr)
}

func TestSlice1(t *testing.T) {
	arr := make([]int, 1, 1)
	arr[0] = 1
	changeSlice1(arr)
	fmt.Printf("arr: %v\n", arr)
}

map作为函数参数

go 复制代码
package mapdemo

import "testing"

func changeMap0(m map[string]int) {
	m["a"] = 1
	m["b"] = 2
	m["c"] = 3
	m["d"] = 4
	m["e"] = 5
	m["f"] = 6
	m["g"] = 7
	m["h"] = 8
	m["i"] = 9
	m["j"] = 10
	m["k"] = 11
	m["l"] = 12
	m["m"] = 13
	m["n"] = 14
	m["o"] = 15
	m["p"] = 16
}

func TestChangeMap(t *testing.T) {
	m := make(map[string]int)
	changeMap0(m)

	t.Log(m)
}

在go语言中,只有值传递,没有引用传递。使用slice和map作为函数参数时,传递的是slice和map的拷贝。而通过上面的讲解我们知道:slice本身是结构体,而 map本身是指针。 所以,当我们传递slice时,是把slice成员变量的值赋给了形参(也就是slice的拷贝),包含底层数组指针、长度值、容量值,如果我们在函数中 修改了slice的成员变量,那实际上是修改了slice的拷贝,而不是原来的slice。只不过,如果slice没有发生扩容,那么slice底层还是指向原来的数组,所以在 函数内部修改slice某个元素的值,会影响到原来的slice。但是,如果slice发生了扩容,那么slice底层指向的就是一个新的数组,所以在函数内部修改slice某 个元素的值,不会影响到原来的slice。 而当传递map时,是把hmap结构体的指针赋给了形参,所以在函数内部修改map,会影响到原来的map。

new

new用于为任何类型分配内存,并返回指向该类型的指针。 使用new来创建map、slice和channel时,并不会初始化内部的数据结构。

  • 对于slice,返回的是一个指向底层结构体的指针,该结构体的成员变量都是零值。
  • 对于map和chan来说,返回的是一个指向指针的指针,由于没有初始化, 其指向的就是一个nil指针。

new创建的map和chan是无法直接使用的。

本文由mdnice多平台发布

相关推荐
前端中后台9 小时前
Cursor实践使用技巧
人工智能·程序员
程序员鱼皮11 小时前
突发,小红书开发者后门被破解?!
计算机·程序员·编程·事故
CodeSheep13 小时前
稚晖君公司再获新投资,yyds!
前端·后端·程序员
袁煦丞16 小时前
AI配音情感魔术师ChatTTS: cpolar内网穿透实验室第414个成功挑战
前端·程序员·远程工作
DeepSeek忠实粉丝17 小时前
微调篇--Transformers多模态流水线任务
人工智能·程序员·llm
LLM大模型1 天前
LangGraph篇-LangGraph快速入门
人工智能·程序员·llm
LLM大模型1 天前
LangGraph篇-核心组件
人工智能·程序员·llm
hotdogc10171 天前
Zed 和 Cursor 的 AI 到底谁才是未来?
程序员
天天摸鱼的java工程师1 天前
前端难还是后端难?作为八年后端开发,我想说点实话
前端·后端·程序员
沉默王二2 天前
自从切换到字节的TRAE Pro 版,编程真的爽的起飞。
人工智能·后端·程序员