程序的運行都需要內(nèi)存,比如變量的創(chuàng)建蔚出、函數(shù)的調(diào)用询刹、數(shù)據(jù)的計算等。所以在需要內(nèi)存的時候就需要申請內(nèi)存湘纵,進(jìn)行內(nèi)存分配。在C/C++這類語言中滤淳,內(nèi)存是由開發(fā)者自己管理的梧喷,需要主動申請和釋放,而在Go語言中則是由該語言自己管理的脖咐,開發(fā)者不用關(guān)心太多铺敌,只需要聲明變量,Go語言就會根據(jù)變量的類型自動分配相應(yīng)的內(nèi)存屁擅。
Go語言程序所管理的虛擬內(nèi)存空間被分為兩個部分:**堆內(nèi)存和棧內(nèi)存**偿凭。棧內(nèi)存主要有Go語言來管理,開發(fā)者無法干涉太多派歌,堆內(nèi)存才是我們開發(fā)者的舞臺笔喉,因為程序的數(shù)據(jù)大部分分配在堆內(nèi)存上,一個程序的大部分內(nèi)存占用也是在堆內(nèi)存上硝皂。
提示: 我們常說的Go語言的內(nèi)存垃圾回收是針對堆內(nèi)存的垃圾回收常挚。
變量的聲明、初始化就涉及內(nèi)存的分配稽物,比如聲明變量會用到var關(guān)鍵字奄毡,如果要對變量初始化,就會用到 = 賦值運算符贝或、除此之外還可以使用內(nèi)置函數(shù)make和new吼过,這兩個函數(shù)功能非常相似,但是可能還會有些迷惑咪奖,接下來讓我們基于內(nèi)存分配盗忱,引出內(nèi)置函數(shù)make和new,講講它們的不同羊赵,以及使用場景趟佃。
變量
一個數(shù)據(jù)類型,在聲明初始化后都會被賦值給一個變量昧捷,變量存儲了程序運行所需的數(shù)據(jù)闲昭。
變量的聲明
// 我們來復(fù)習(xí)一下變量的聲明
var s string
// 這個例子中,我們只是聲明了一個變量s靡挥,類型為string序矩,并沒有對它進(jìn)行初始化,所以它的值是string的零值跋破,也就是 ""(空字符串)
var sp *string
// 我們聲明了一個指針類型的變量簸淀,也沒有初始化瓶蝴,所以它的值是*string的零值,即nil
變量的賦值
變量可以通過=賦值運算符租幕,修改變量的值舷手。如果在聲明一個變量的時候就給這個變量賦值,這種操作稱為變量的初始化令蛉。如果要對一個變量初始化聚霜,可以有三種辦法
- 聲明時直接初始化,var s string = "奔跑的蝸牛"
- 聲明后在初始化珠叔,s = "奔跑的蝸牛" // (假設(shè)已經(jīng)聲明了變量s)
- 使用簡短聲明蝎宇,如 s:= "奔跑的蝸牛"
提示:變量的初始化也是一種賦值,只不過發(fā)生在變量聲明的時候祷安,時機(jī)最靠前姥芥。即獲得這個變量時,就已經(jīng)被賦值了
那么汇鞭,指針類型的變量能直接賦值嗎凉唐?
func main() {
var sp *string
*sp = "奔跑的蝸牛"
fmt.Println(*sp)
}
// 上述代碼運行會報錯,錯誤信息如下:
painc: runtime error: invalid memory address or nil pointer dereference
// 因為指針類型的變量如果沒有分配內(nèi)存霍骄,就默認(rèn)值是nil台囱,它沒有指向的內(nèi)存,所以無法師用读整,強(qiáng)行使用會得到 nil指針錯誤簿训。 對于值類型來說,即使只是聲明一個變量米间,并沒有對其初始化强品,該變量也會有分配好的內(nèi)存。
func main() {
var s string
fmt.Println(&s)
}
// 我們可以獲取到變量s的內(nèi)存地址屈糊,這是Go語言幫我們做的的榛,可以直接使用。
var wg sync.WaitGroup
聲明的變量wg逻锐,我們并沒有對其初始化就可以使用了夫晌,因為sync.WaitGroup 是一個 struct 結(jié)構(gòu)體,是一個值類型谦去,Go語言自動分配了內(nèi)存慷丽,所以可以直接使用,不會報nil異常鳄哭。
小結(jié):如果要對一個變量賦值,這個變量必須有對應(yīng)的分配好的內(nèi)存纲熏,這樣才可以對這塊內(nèi)存操作妆丘,完成賦值操作锄俄。對于指針變量,如果沒有分配內(nèi)存勺拣,取值操作也一樣會報nil異常奶赠,因為沒有可以操作的內(nèi)存。 所以药有,一個變量必須要經(jīng)過聲明毅戈、內(nèi)存分配才能賦值,才可以在聲明的時候進(jìn)行初始化愤惰。指針類型在聲明的時候苇经,Go語言沒有自動分配好內(nèi)存,所以不能對其進(jìn)行賦值操作宦言,這一點和值類型不一樣扇单。
提示: map和chan也一樣,它們本質(zhì)上也是指針類型奠旺。
new函數(shù)
我們知道了聲明的指針變量是沒有分配內(nèi)存的蜘澜,那么如何給它分配一塊呢? 那就是通過**內(nèi)置new函數(shù)**响疚。
func main() {
var sp *string
sp = new(string) // 關(guān)鍵點鄙信,通過內(nèi)置new函數(shù)生成了一個*string,并賦值給了變量sp忿晕。
*sp = "奔跑的蝸牛"
fmt.Println(*sp)
}
// 內(nèi)置new的作用是什么呢装诡? 我們來分析一下源碼
func new(Type) *Type
**new函數(shù)**的作用就是根據(jù)傳入的類型申請一塊內(nèi)存,然后返回指向這塊內(nèi)存的指針杏糙,指針指向的數(shù)據(jù)就是該類型的零值慎王。比如傳入的類型是string,那么返回的就是string指針宏侍,這個string指針指向的數(shù)據(jù)就是空字符串赖淤。
spl = new(string)
fmt.Println(*spl) // 打印空字符串,也就是string的零值谅河。
通過new函數(shù)分配內(nèi)存并返回指向該內(nèi)存的指針后咱旱,就可以通過該指針對這塊內(nèi)存進(jìn)行賦值、取值等操作绷耍。
變量初始化
當(dāng)聲明了一些類型的變量時吐限,這些變量的零值并不能滿足我們需要,這時需要在變量聲明同時進(jìn)行賦值(修改變量的值)褂始,這個過程稱為變量初始化诸典。
不止基礎(chǔ)類型可以通過字面量的方式進(jìn)行初始化,復(fù)合類型也可以崎苗,比如結(jié)構(gòu)體狐粱。
type person struct{
name string
age int
}
func main() {
// 字面量初始化
p := person{name:"zhangsan", age:18}
}
指針變量初始化
我們知道new函數(shù)可以申請內(nèi)存并返回一個指向該內(nèi)存的指針舀寓,但是這塊內(nèi)存中數(shù)據(jù)的默認(rèn)值是該類型的零值,在一些情況下并不滿足需要肌蜻。如果我們想要獲得一個*person類型的指針互墓,并且初始化,但是new函數(shù)只有一個類型參數(shù)蒋搜,并沒有初始化值的參數(shù)篡撵,怎么辦呢? 可以自定義一個函數(shù)豆挽,對指針變量進(jìn)行初始化育谬。
func NewPerson() *person {
p := new(person)
p.name = "zhangsan"
p.age = 18
return p
}
p := NewPerson()
fmt.Println("name:", p.name, "age:", p.age)
// 這就是工廠函數(shù),NewPerson 函數(shù)就是工廠函數(shù)祷杈,除了使用new函數(shù)創(chuàng)建了指針外斑司,還進(jìn)行了賦值,即初始化但汞。通過NewPerson函數(shù)做了一層包裝宿刮,把內(nèi)存分配(new 函數(shù))和初始化(賦值)都完成了。
make函數(shù)
接下來講講make函數(shù)私蕾。我們知道僵缺,使用make函數(shù)創(chuàng)建map的時候,其實調(diào)用的是makehmap函數(shù)踩叭。
func makemap(t *maptype, hint int ,h *hmap) *hmap {}
// makemap函數(shù)返回的是*hmap類型磕潮,而hmap是一個結(jié)構(gòu)體,源碼如下
type hmap struct {
count int
flags uint8
B uint8
noverflow uint16
hash0 uint16
buckets unsafe.Pointer
oldbuckets unsafe.Pointer
nevacuate uintptr
extra *mapextra
}
m := make(map[string]int, 10)
// make函數(shù)和我們的自定義NewPerson函數(shù)很像容贝。其實make函數(shù)就是map類型的工廠函數(shù)自脯,它可以根據(jù)傳遞它的K-V鍵值對類型,創(chuàng)建不同類型的map斤富,同時可以初始化map的大小膏潮。
可以看到,我們平常使用的map關(guān)鍵字非常復(fù)雜满力,包含map的大小count焕参、存儲桶buckets等。要想這樣使用hmap油额,不是簡單的通過new函數(shù)返回一個*hmap就可以叠纷,還需要對其初始化,這就是make函數(shù)要做的事情潦嘶。
提示: make函數(shù)不只是map類型的工廠函數(shù)涩嚣,還是chan、slice的工廠函數(shù)。它同時可以用于slice缓艳、chan和map三種類型的初始化校摩。
總結(jié):new和make函數(shù)的區(qū)別如下:
1. new函數(shù)只用于分配內(nèi)存看峻,并且把內(nèi)存清零阶淘,也就是返回一個指向?qū)?yīng)類型零值的指針。new函數(shù)一般用于需要顯示的返回指針的情況互妓,不是太常用溪窒。**new返回的是對象的指針,對指針?biāo)趯ο蟮母姆朊悖瑫绊懼羔樦赶虻脑紝ο蟮闹?*澈蚌。
2. make函數(shù)只用于slice、chan和map這三種內(nèi)置類型的創(chuàng)建和初始化灼狰,因為這三種類型的結(jié)構(gòu)比較復(fù)雜宛瞄,比如slice要提前初始化號內(nèi)部元素的類型、slice的長度和容量等交胚,這樣才能更好地使用它們份汗。**make返回的是對象**
* 對值類型對象的修改,不會影響原始對象的值
* 對引用類型的修改蝴簇,會影響原始對象的值