Golang 中該使用指針類型還是值類型

使用 Golang 的開發(fā)者都知道,Go 語言里有指針的概念梢为,它比 C++ 的指針要簡單的多渐行,同時(shí)你需要記住一個(gè)概念:Go 語言是 值傳遞轰坊。我們今天探討的是在編碼的時(shí)候到底該使用指針呢還是值類型?在作為參數(shù)和返回值的時(shí)候該如何去使用祟印?兩種傳遞方式有什么區(qū)別肴沫?

基礎(chǔ)概念

值類型和引用類型

這幅圖中展示了常用的值類型和引用類型(引用類型和傳引用是兩個(gè)概念)。在左邊是我們常用的一些值類型蕴忆,函數(shù)調(diào)用時(shí)需要使用指針修改底層數(shù)據(jù)颤芬;而右邊是 “引用類型”,我們可以理解為它們的底層都是指針類型套鹅,所以右邊的類型在使用的時(shí)候會(huì)有些不同站蝠,下文中會(huì)舉例說明。

舉個(gè)栗子

type Foo struct {
    Name string
}

var bar = "hello biezhi"

// -------------方法返回值----------------
func returnValue() string {
    return bar
}

func returnPoint() *string {
    return &bar
}

// --------------方法入?yún)?----------------
func modifyNameByPoint(foo *Foo) {
    foo.Name = "emmmm " + foo.Name
}

func nameToUpper(foo Foo) string {
    foo.Name = strings.ToUpper(foo.Name)
    return foo.Name
}

// --------------實(shí)例方法-----------------
func (foo Foo) setName(name string) {
    foo.Name = name
}

func (foo *Foo) setNameByPoint(name string) {
    foo.Name = name
}

這里我列出了 3 組方法卓鹿,分別是指針類型和值類型的示例菱魔。這幾個(gè)方法在編寫代碼的過程中都會(huì)經(jīng)常遇到,我們從使用者的維度和內(nèi)存的視角來分析一下這幾個(gè)方法:

使用區(qū)別

大部分人都在討論函數(shù)的入?yún)⑹侵羔樳€是值類型呢吟孙?我們先來看看第一組方法澜倦,返回值的情況:

s1 := returnValue()
s2 := returnPoint()

fmt.Printf("s1: %v \n", s1)   // s1: hello biezhi 
fmt.Printf("s2: %v \n", *s2)  // s2: hello biezhi 

這兩個(gè)方法一個(gè)返回了指針一個(gè)返回值類型,值類型是非 nil 的(在 Go 中所有的值類型都會(huì)有 初值)杰妓,指針類型可以判斷是否為 nil藻治。 獲取到的數(shù)據(jù)是相同的,不同之處在于取值的方式巷挥,指針類型需要使用 * 號(hào)讀取數(shù)據(jù)桩卵。


下面嘗試傳遞參數(shù),分別是指針類型參數(shù)和值類型參數(shù):

foo := Foo{Name:"biezhi"}
fmt.Println("foo.name:", foo.Name)          // foo.name: biezhi

modifyNameByPoint(&foo)
fmt.Println("foo.name:", foo.Name)          // foo.name: emmmm biezhi

fmt.Println("foo.name:", nameToUpper(foo))  // foo.name: EMMMM BIEZHI
fmt.Println("foo.name:", foo.Name)          // foo.name: emmmm biezhi

  • modifyNameByPoint 需要指針類型倍宾,所以我們?nèi)?foo 的指針傳入(foo 是值類型所以這里用 & 取其地址)雏节。
  • nameToUpper 需要一個(gè)值類型的參數(shù),所以 foo 直接傳入凿宾,返回值是轉(zhuǎn)大寫的 Name矾屯。
  • nameToUpper 不會(huì)修改 foo.Name 的數(shù)據(jù)兼蕊,最后一次輸出還是舊數(shù)據(jù)

綜上例子初厚,我們可以看出 指針類型會(huì)修改指向的數(shù)據(jù)值類型的數(shù)據(jù)只有在返回的時(shí)候被使用孙技,不會(huì)修改底層數(shù)據(jù)产禾。

Go 中是值傳遞,一個(gè)方法 / 函數(shù)總是獲取這個(gè)傳遞的拷貝牵啦,只是有一個(gè)分配聲明給這個(gè)參數(shù)分配這個(gè)數(shù)值亚情。拷貝一個(gè)指針的值就做了這個(gè)指針的拷貝哈雏,而不是指針指向的數(shù)據(jù)(重點(diǎn)理解)楞件。

內(nèi)存變化

我們使用值類型和指針類型在內(nèi)存的視角上會(huì)有什么不同之處嗎衫生?這將使得我們對(duì)這兩個(gè)概念理解更加深入。

返回值的情況

var bar = "hello biezhi"
s1 := returnValue()
s2 := returnPoint()

fmt.Printf("bar: %v address: %p \n", bar, &bar) // 1
fmt.Printf("s1 : %v address: %p \n", s1, &s1)   // 2
fmt.Printf("s2 : %v address: %p \n", *s2, &s2)  // 3

// output
? bar: hello biezhi address: 0x115f480 
? s1 : hello biezhi address: 0xc00000e1e0 
? s2 : hello biezhi address: 0xc00000c030 

從這個(gè)輸出中可以看到數(shù)據(jù)都是一樣的土浸,這里使用 %p 輸出一個(gè)指針的值(內(nèi)存地址)都不同罪针。第一個(gè)毋庸置疑是初始的內(nèi)存地址,s1 是調(diào)用返回值類型的結(jié)果黄伊,s2 是返回指針類型的結(jié)果泪酱。照這樣看的話好像返回指針還是值類型沒有區(qū)別,地址都是新的还最。

來分析一下墓阀,首先 s1 的內(nèi)存地址發(fā)生變化是因?yàn)榉椒▍?shù)被拷貝后產(chǎn)生了一份新的值給 s1,此時(shí) s1 分配了新地址拓轻。對(duì)于 s2 也拷貝了一份新值斯撮,只不過這個(gè)值是 指針類型,所以在取數(shù)據(jù)的時(shí)候用了 *扶叉。

既然都分配了地址吮成,到底使用值類型還是指針類型作為返回值,我的推薦是這樣的:

  • 當(dāng)返回類型不涉及狀態(tài)變更并且是較簡單的數(shù)據(jù)結(jié)構(gòu)辜梳,一律返回值類型
  • 當(dāng)返回類型可能遇到狀態(tài)變更或者你關(guān)心它的生命周期則使用指針類型
  • 當(dāng)返回的結(jié)構(gòu)比較大的時(shí)候使用指針類型

方法參數(shù)情況

我們?cè)?nameToUpper 中添加一句輸出:

func nameToUpper(foo Foo) string {
    foo.Name = strings.ToUpper(foo.Name)
    fmt.Printf("nameToUpper foo: %v address: %p \n", foo, &foo) // 2
    return foo.Name
}
foo := Foo{Name:"biezhi"}
fmt.Printf("foo: %v address: %p \n", foo, &foo) // 1

nameToUpper(foo)
fmt.Printf("foo: %v address: %p \n", foo, &foo) // 3

// output
? foo: {biezhi} address: 0xc00000e1e0 
? nameToUpper foo: {BIEZHI} address: 0xc00000e200
? foo: {biezhi} address: 0xc00000e1e0

nameToUpper 接收值類型的參數(shù)粱甫,觀察輸出你會(huì)發(fā)現(xiàn)在外部的 foo 變量內(nèi)存地址是沒有發(fā)生變化的。

在方法內(nèi)部接收這個(gè) 值類型變量 的時(shí)候作瞄,內(nèi)存地址和外面不同茶宵,這意味著 Go 會(huì)將這個(gè)值類型參數(shù)作為一個(gè)拷貝傳遞過去,在方法內(nèi)部的改變都不會(huì)影響到外面的變量宗挥。

如果方法接收一個(gè)指針類型呢乌庶?來試試 modifyNameByPoint 方法:

func modifyNameByPoint(foo *Foo) {
    fmt.Printf("modifyNameByPoint foo: %v address: %p \n", foo, &foo) // 2
    foo.Name = "emmmm " + foo.Name
}
foo := &Foo{Name:"biezhi"}
fmt.Printf("foo: %v address: %p \n", foo, &foo) // 1

modifyNameByPoint(foo)
fmt.Printf("foo: %v address: %p \n", foo, &foo) // 3

// output
? foo: &{biezhi} address: 0xc00000c028 
? modifyNameByPoint foo: &{biezhi} address: 0xc00000c038 
? foo: &{emmmm biezhi} address: 0xc00000c028

可以看到,數(shù)據(jù)被修改了契耿,因?yàn)閭鬟f的是指針瞒大;內(nèi)存地址沒有發(fā)生變化,作為入?yún)⒌?foo 在方法內(nèi)部的地址也是一份新的拷貝搪桂,這一點(diǎn)和前面返回值是相同的(0xc00000c0280xc00000c038 指向同一份數(shù)據(jù))透敌。

Receiver Type

如果你編寫 Java 代碼的話經(jīng)常會(huì)看到這樣的代碼

public class Bar {
    String name;
    public void setName(String name){
        this.name = name;
    }
}

可以看到這里有 this 關(guān)鍵字,在 Go 中是沒有的踢械,這里的 this 可以調(diào)用當(dāng)前對(duì)象的成員變量和實(shí)例方法酗电,當(dāng)使用 this 修改了成員變量就相當(dāng)于在 Go 中使用了指針,看看下面的 Go 代碼:

func (foo *Foo) setNameByPoint(name string) {
    foo.Name = name
}

func (foo Foo) setName(name string) {
    foo.Name = name
}

Go 中想要為結(jié)構(gòu)體定義屬于自己的方法就使用如上的兩種方式内列,這兩個(gè)方法在 Go 中稱為 Receiver Type(接受者類型)撵术,可以使用結(jié)構(gòu)體變量調(diào)用,我們今天只討論結(jié)構(gòu)體這種情況话瞧,來看看這兩個(gè)方法有什么不同:

foo := Foo{Name:"biezhi"}
foo.setName("2333")
fmt.Println("foo.Name:", foo.Name)  // foo.Name: biezhi

foo.setNameByPoint("2333")
fmt.Println("foo.Name:", foo.Name)  // foo.Name: 2333

根據(jù)輸出發(fā)現(xiàn)一個(gè)結(jié)構(gòu)體嫩与,如果不使用指針類型的時(shí)候值是不會(huì)被修改的寝姿。這點(diǎn)也很容易理解,在 setName方法中 foo 變量被作為值傳遞划滋,所以如果這時(shí)候輸出 foo 的內(nèi)存地址會(huì)發(fā)現(xiàn)和外面調(diào)用的是不一樣的会油,來看看:

func (foo Foo) setName(name string) {
    fmt.Printf("setName: %v address: %p \n", foo, &foo) // 2
    foo.Name = name
}

func (foo *Foo) setNameByPoint(name string) {
    fmt.Printf("setNameByPoint: %v address: %p \n", foo, &foo) // 4
    foo.Name = name
}
foo := Foo{Name:"biezhi"}
fmt.Printf("src foo: %v address: %p \n", foo, &foo)         // 1

foo.setName("set name")
fmt.Printf("by value foo: %v address: %p \n", foo, &foo)    // 3

foo.setNameByPoint("2333")
fmt.Printf("by point foo: %v address: %p \n", foo, &foo)    // 5

// output
? src foo: {biezhi} address: 0xc00000e1e0 
? setName: {biezhi} address: 0xc00000e200
? by value foo: {biezhi} address: 0xc00000e1e0 
? setNameByPoint: &{biezhi} address: 0xc00000c030 
? by point foo: {2333} address: 0xc00000e1e0

setNameByPoint 方法和前面的指針類型傳遞是一樣的,方法內(nèi)部內(nèi)存地址是一份指針的拷貝古毛,修改數(shù)據(jù)會(huì)影響到外部指針變量的數(shù)據(jù)翻翩。

一般而言,工程化的項(xiàng)目中會(huì)出現(xiàn)非常多結(jié)構(gòu)體定義方法的代碼稻薇,這些方法的調(diào)用也會(huì)很頻繁嫂冻,使用結(jié)構(gòu)體將其封裝起來,和 Java 中類封裝是一樣的塞椎,大多數(shù)情況下建議都使用指針傳遞桨仿,避免值拷貝的情況。

其他類型

在前面我們有一張圖中分了值類型和引用類型案狠,除了那些常用的基本類型服傍,還有像 mapslice 這種引用類型,它們?cè)谑褂蒙嫌悬c(diǎn)像指針(但不用任何操作符如 &骂铁、*)吹零,來看個(gè)例子:

func updateMap(mmp map[string]int)  {
    mmp["biezhi"] = 2333
}

func main() {
    mmp := make(map[string]int)
    mmp["biezhi"] = 1024
    fmt.Printf("src mmp: %v address: %p \n", mmp, &mmp) // 1

    updateMap(mmp)
    fmt.Printf("new mmp: %v address: %p \n", mmp, &mmp) // 2
}

// output
? src mmp: map[biezhi:1024] address: 0xc000094018 
? new mmp: map[biezhi:2333] address: 0xc000094018

如果你嘗試 slice 的話是同樣的效果,可以看到給方法傳遞的并非是一個(gè)指針類型拉庵,但是 map 的值確實(shí)被修改了灿椅,這是為什么呢?

其實(shí)拷貝一個(gè) map 或者 slice 的時(shí)候并沒有拷貝這個(gè)類型(引用類型)里面指向的數(shù)據(jù)钞支,而是拷貝了引用類型(可簡單理解為指針)茫蛹,如何驗(yàn)證這一說法呢?我們?cè)?updateMap 中添加一行輸出代碼:

func updateMap(mmp map[string]int) {
    fmt.Printf("param mmp: %v address: %p \n", mmp, &mmp)
    mmp["biezhi"] = 2333
}

再次運(yùn)行代碼

src mmp: map[biezhi:1024] address: 0xc000094018 
input mmp: map[biezhi:1024] address: 0xc00000c038 
new mmp: map[biezhi:2333] address: 0xc000094018

你會(huì)發(fā)現(xiàn) input mmp 這行的地址發(fā)生了變化烁挟,正因?yàn)榭截惖氖沁@個(gè)特殊的 “引用類型”婴洼,會(huì)產(chǎn)生一個(gè)新的地址,而這個(gè)地址 0xc00000c0380xc000094018 指向的是同一份數(shù)據(jù)撼嗓,所以修改后外部的變量也會(huì)得到新的數(shù)據(jù)柬采。

小結(jié)

前面我們通過一些代碼示例來演示了在 Go 中值類型和指針類型的一些具體表現(xiàn),最后我們要回答這么幾個(gè)問題静稻,希望你能夠在使用 Go 編程的過程中更加清晰的掌握這些技巧警没。

Receiver Type 為什么推薦使用指針匈辱?

  • 推薦在實(shí)例方法上使用指針(前提是這個(gè)類型不是一個(gè)自定義的 map振湾、slice 等引用類型)
  • 當(dāng)結(jié)構(gòu)體較大的時(shí)候使用指針會(huì)更高效
  • 如果要修改結(jié)構(gòu)內(nèi)部的數(shù)據(jù)或狀態(tài)必須使用指針
  • 當(dāng)結(jié)構(gòu)類型包含 sync.Mutex 或者同步這種字段時(shí),必須使用指針以避免成員拷貝
  • 如果你不知道該不該使用指針亡脸,使用指針押搪!

“結(jié)構(gòu)較大” 到底多大才算大可能需要自己或團(tuán)隊(duì)衡量树酪,如超過 5 個(gè)字段或者根據(jù)結(jié)構(gòu)體內(nèi)占用來計(jì)算。

方法參數(shù)該使用什么類型大州?

  • map续语、slice 等類型不需要使用指針(自帶 buf)
  • 指針可以避免內(nèi)存拷貝,結(jié)構(gòu)大的時(shí)候不要使用值類型
  • 值類型和指針類型在方法內(nèi)部都會(huì)產(chǎn)生一份拷貝厦画,指向不同
  • 小數(shù)據(jù)類型如 bool疮茄、int 等沒必要使用指針傳遞
  • 初始化一個(gè)新類型時(shí)(像 NewEngine() *Engine)使用指針
  • 變量的生命周期越長則使用指針,否則使用值類型

參考資料

轉(zhuǎn)載于

https://blog.biezhi.me/2018/10/values-or-pointers-in-golang.html
https://keer.me/values-or-pointers-in-golang.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末根暑,一起剝皮案震驚了整個(gè)濱河市力试,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌排嫌,老刑警劉巖畸裳,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異淳地,居然都是意外死亡怖糊,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門颇象,熙熙樓的掌柜王于貴愁眉苦臉地迎上來伍伤,“玉大人,你說我怎么就攤上這事遣钳∪络裕” “怎么了?”我有些...
    開封第一講書人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵耍贾,是天一觀的道長阅爽。 經(jīng)常有香客問我,道長荐开,這世上最難降的妖魔是什么付翁? 我笑而不...
    開封第一講書人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮晃听,結(jié)果婚禮上百侧,老公的妹妹穿的比我還像新娘。我一直安慰自己能扒,他們只是感情好佣渴,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著初斑,像睡著了一般辛润。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上见秤,一...
    開封第一講書人閱讀 51,541評(píng)論 1 305
  • 那天砂竖,我揣著相機(jī)與錄音真椿,去河邊找鬼。 笑死乎澄,一個(gè)胖子當(dāng)著我的面吹牛突硝,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播置济,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼解恰,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了浙于?” 一聲冷哼從身側(cè)響起修噪,我...
    開封第一講書人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎路媚,沒想到半個(gè)月后黄琼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡整慎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年脏款,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片裤园。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡撤师,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出拧揽,到底是詐尸還是另有隱情剃盾,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布淤袜,位于F島的核電站痒谴,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏铡羡。R本人自食惡果不足惜积蔚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望烦周。 院中可真熱鬧尽爆,春花似錦、人聲如沸读慎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽夭委。三九已至幅狮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背彪笼。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來泰國打工钻注, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蚂且,地道東北人配猫。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像杏死,于是被迫代替她去往敵國和親泵肄。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容