直到目前(Go 1.19), Go中對(duì)短變量聲明有一個(gè)強(qiáng)制性約束:
所有位于?
:=
?符號(hào)左側(cè)的條目都必須是純標(biāo)識(shí)符,并且其中至少有一個(gè)為新變量名稱(chēng)。
這意味著容器元素索引表達(dá)式(x[i]
)、結(jié)構(gòu)體的字段選擇器(x.f
)、指針解引用(*p
)和限定標(biāo)識(shí)符(aPackage.Value
)都不能出現(xiàn)在:=
符號(hào)的左側(cè)。
目前,這還是一個(gè)未解決問(wèn)題(已經(jīng)和一個(gè)相關(guān)問(wèn)題合并)。而且感覺(jué)Go核心開(kāi)發(fā)團(tuán)隊(duì)目前并未有立即解決此問(wèn)題的打算。
在編寫(xiě)Go代碼時(shí),我們不能隨意斷行。 請(qǐng)閱讀代碼斷行規(guī)則一文以了解Go代碼斷行規(guī)則。 一般來(lái)說(shuō),根據(jù)這些規(guī)則,在左括號(hào)之前斷行是不合法的。
例如,下列代碼片段
if true
{
}
for i := 0; i < 10; i++
{
}
var _ = []int
{
1, 2, 3
}
將會(huì)被編譯器解釋成
if true;
{
}
for i := 0; i < 10; i++;
{
}
var _ = []int;
{
1, 2, 3;
}
Go編譯器將為每個(gè)左大括號(hào){
起始的代碼行報(bào)告一個(gè)語(yǔ)法錯(cuò)誤。 為避免這些報(bào)錯(cuò),我們需要將上述代碼重寫(xiě)為下面這樣:
if true {
}
for i := 0; i < 10; i++ {
}
var _ = []int {
1, 2, 3,
}
對(duì)于標(biāo)準(zhǔn)編譯器,在局部代碼塊中聲明的每一個(gè)變量必須被至少一次用做r-value(right-hand-side value,右值)。
因此,下列代碼將編譯失敗,因?yàn)?code>y只被用做目標(biāo)值(目標(biāo)值都為左值)。
func f(x bool) {
var y = 1 // y被聲明了但沒(méi)有被用做右值
if x {
y = 2 // 這里,y被用做左值
}
}
不。Go白皮書(shū)明確提到映射元素的迭代順序是未定義的。 所以對(duì)于同一個(gè)映射值,它的一個(gè)遍歷過(guò)程和下一個(gè)遍歷過(guò)程中的元素呈現(xiàn)次序不保證是相同的。 對(duì)于標(biāo)準(zhǔn)編譯器,映射元素的遍歷順序是隨機(jī)的。 如果你需要固定的映射元素遍歷順序,那么你就需要自己來(lái)維護(hù)這個(gè)順序。 更多信息請(qǐng)閱讀Go官方博客文章Go maps in action。
但是請(qǐng)注意:從Go 1.12開(kāi)始,標(biāo)準(zhǔn)庫(kù)包中的各個(gè)打印函數(shù)的結(jié)果中,映射條目總是排了序的。
至少對(duì)于標(biāo)準(zhǔn)的Go編譯器和gccgo,答案是肯定的。 具體需要填充多少個(gè)字節(jié)取決于操作系統(tǒng)和編譯器實(shí)現(xiàn)。 請(qǐng)閱讀關(guān)于Go值的內(nèi)存布局一文獲取詳情。
Go編譯器將不會(huì)重新排列結(jié)構(gòu)體的字段來(lái)最小化結(jié)構(gòu)體值的尺寸。 因?yàn)檫@樣做會(huì)導(dǎo)致意想不到的結(jié)果。 但是,根據(jù)需要,程序員可以手工重新排序字段來(lái)實(shí)現(xiàn)填充最小化。
一個(gè)可尋址的結(jié)構(gòu)值的所有字段都可以被取地址。 如果非零尺寸的結(jié)構(gòu)體值的最后一個(gè)字段的尺寸是零,那么取此最后一個(gè)字段的地址將會(huì)返回一個(gè)越出了為此結(jié)構(gòu)體值分配的內(nèi)存塊的地址。 這個(gè)返回的地址可能指向另一個(gè)被分配的內(nèi)存塊。 在目前的官方Go標(biāo)準(zhǔn)運(yùn)行時(shí)的實(shí)現(xiàn)中,如果一個(gè)內(nèi)存塊被至少一個(gè)依然活躍的指針引用,那么這個(gè)內(nèi)存塊將不會(huì)被視作垃圾因而肯定不會(huì)被回收。 所以只要有一個(gè)活躍的指針存儲(chǔ)著此非零尺寸的結(jié)構(gòu)體值的最后一個(gè)字段的越界地址,它將阻止垃圾收集器回收另一個(gè)內(nèi)存塊,從而可能導(dǎo)致內(nèi)存泄漏。
為避免上述問(wèn)題,標(biāo)準(zhǔn)的Go編譯器會(huì)確保取一個(gè)非零尺寸的結(jié)構(gòu)體值的最后一個(gè)字段的地址時(shí),絕對(duì)不會(huì)返回越出分配給此結(jié)構(gòu)體值的內(nèi)存塊的地址。 Go標(biāo)準(zhǔn)編譯器通過(guò)在需要時(shí)在結(jié)構(gòu)體最后的零尺寸字段之后填充一些字節(jié)來(lái)實(shí)現(xiàn)這一點(diǎn)。
如果一個(gè)結(jié)構(gòu)體的全部字段的類(lèi)型都是零尺寸的(因此整個(gè)結(jié)構(gòu)體也是零尺寸的),那么就不需要再填充字節(jié),因?yàn)闃?biāo)準(zhǔn)編譯器會(huì)專(zhuān)門(mén)處理零尺寸的內(nèi)存塊。
一個(gè)例子:
package main
import (
"unsafe"
"fmt"
)
func main() {
type T1 struct {
a struct{}
x int64
}
fmt.Println(unsafe.Sizeof(T1{})) // 8
type T2 struct {
x int64
a struct{}
}
fmt.Println(unsafe.Sizeof(T2{})) // 16
}
雖然這兩者在實(shí)現(xiàn)上會(huì)有一些微妙的差別,取決于編譯器的具體實(shí)現(xiàn),但是我們基本上可以認(rèn)為這兩者是等價(jià)的。 即,通過(guò)new
函數(shù)分配的內(nèi)存塊可以在棧上,也可以在堆上。
用詞asleep在這里其實(shí)并不準(zhǔn)確,實(shí)際上它的意思是處于阻塞狀態(tài)。
因?yàn)橐粋€(gè)處于阻塞狀態(tài)的協(xié)程只能被另一個(gè)協(xié)程解除阻塞,如果程序中所有的協(xié)程都進(jìn)入了阻塞狀態(tài),則它們將永遠(yuǎn)都處于阻塞狀態(tài)。 這意味著程序死鎖了。一個(gè)正常運(yùn)行的程序永遠(yuǎn)不應(yīng)該死鎖,一個(gè)死鎖的程序肯定是由于邏輯實(shí)現(xiàn)上的bug造成的。 因此官方Go標(biāo)準(zhǔn)運(yùn)行時(shí)將在一個(gè)程序死鎖時(shí)令其崩潰退出。
傳遞給sync/atomic
標(biāo)準(zhǔn)庫(kù)包中的64位函數(shù)的地址必須是64位對(duì)齊的,否則調(diào)用這些函數(shù)將在運(yùn)行時(shí)導(dǎo)致恐慌產(chǎn)生。
對(duì)于標(biāo)準(zhǔn)編譯器和gccgo編譯器,在64位架構(gòu)下,64位整數(shù)的地址將保證總是64位對(duì)齊的。 所以它們總是可以被安全地原子訪問(wèn)。 但在32位架構(gòu)下,64位整數(shù)的地址僅保證是32位對(duì)齊的。 所以原子訪問(wèn)某些64位整數(shù)可能會(huì)導(dǎo)致恐慌。 但是,有一些方法可以保證一些64位整數(shù)總是可以被安全地原子訪問(wèn)。 請(qǐng)閱讀關(guān)于Go值的內(nèi)存布局一文以獲得詳情。
對(duì)于標(biāo)準(zhǔn)編譯器來(lái)說(shuō),賦值不是原子操作。
請(qǐng)閱讀官方FAQ中的此問(wèn)答以了解更多。
對(duì)于大部分類(lèi)型,答案是肯定的。不過(guò)事實(shí)上,這依賴(lài)于編譯器。 例如,對(duì)于標(biāo)準(zhǔn)編譯器,對(duì)于某些字符串類(lèi)型的零值,此結(jié)論并不十分正確。
package main
import (
"unsafe"
"fmt"
)
func main() {
var s1 string
fmt.Println(s1 == "") // true
fmt.Println(*(*uintptr)(unsafe.Pointer(&s1))) // 0
var s2 = "abc"[0:0]
fmt.Println(s2 == "") // true
fmt.Println(*(*uintptr)(unsafe.Pointer(&s2))) // 4869856
fmt.Println(s1 == s2) // true
}
反過(guò)來(lái),對(duì)于標(biāo)準(zhǔn)編譯器已經(jīng)支持的所有架構(gòu),如果一個(gè)值的所有字節(jié)都是零,那么這個(gè)值肯定是它的類(lèi)型的零值。 然而,Go規(guī)范并沒(méi)有保證這一點(diǎn)。我曾聽(tīng)說(shuō)在某些比較老的處理器上,空指針表示的內(nèi)存地址并不為零。
是的,標(biāo)準(zhǔn)編譯器支持函數(shù)內(nèi)聯(lián)。編譯器會(huì)自動(dòng)內(nèi)聯(lián)一些滿(mǎn)足某些條件的短小函數(shù)。這些內(nèi)聯(lián)條件可能會(huì)在不同編譯器版本之間發(fā)生變化。
目前(Go 1.19),對(duì)于標(biāo)準(zhǔn)編譯器,
-gcflags "-l"
可以阻止任何函數(shù)被內(nèi)聯(lián), 但是并沒(méi)有一個(gè)正式的方式來(lái)避免某個(gè)特定的用戶(hù)函數(shù)被內(nèi)聯(lián)。 目前我們可以在函數(shù)聲明前增加一行//go:noinline
指令來(lái)避免這個(gè)函數(shù)被內(nèi)聯(lián)。 但是此方式不保證永久有效。在Go程序里,我們可以通過(guò)調(diào)用runtime.SetFinalizer
函數(shù)來(lái)給一個(gè)對(duì)象設(shè)置一個(gè)終結(jié)器函數(shù)。 一般說(shuō)來(lái),此終結(jié)器函數(shù)將在此對(duì)象被垃圾回收之前調(diào)用。 但是終結(jié)器并非被設(shè)計(jì)為對(duì)象的析構(gòu)函數(shù)。 通過(guò)runtime.SetFinalizer
函數(shù)設(shè)置的終結(jié)器函數(shù)并不保證總會(huì)被運(yùn)行。 因此我們不應(yīng)該依賴(lài)于終結(jié)器來(lái)保證程序的正確性。
終結(jié)器的主要用途是為了庫(kù)包的維護(hù)者能夠盡可能地避免因?yàn)閹?kù)包使用者不正確地使用庫(kù)包而帶來(lái)的危害。 例如,我們知道,當(dāng)在程序中使用完某個(gè)文件后,我們應(yīng)該將其關(guān)閉。 但是有時(shí)候因?yàn)榉N種原因,比如經(jīng)驗(yàn)不足或者粗心大意,導(dǎo)致一些文件在使用完成后并未被關(guān)閉,那么和這些文件相關(guān)的很多資源只有在此程序退出之后才能得到釋放。這屬于資源泄漏。 為了盡可能地避免防止資源泄露,os
庫(kù)包的維護(hù)者將會(huì)在一個(gè)os.File
對(duì)象被被創(chuàng)建的時(shí)候?yàn)橹O(shè)置一個(gè)終結(jié)器。 此終結(jié)器函數(shù)將關(guān)閉此os.File
對(duì)象。當(dāng)此os.File
對(duì)象因?yàn)椴辉俦皇褂枚焕厥盏臅r(shí)候,此終結(jié)器函數(shù)將被調(diào)用。
請(qǐng)記住,有一些終結(jié)器函數(shù)永遠(yuǎn)不會(huì)被調(diào)用,并且有時(shí)候不當(dāng)?shù)脑O(shè)置終結(jié)器函數(shù)將會(huì)阻止對(duì)象被垃圾回收。 關(guān)于更多細(xì)節(jié),請(qǐng)閱讀runtime.SetFinalizer函數(shù)的文檔。
假設(shè)輸入的年份是一個(gè)自然年,并且輸入的月份也是一個(gè)自然月(1代表1月)。
days := time.Date(year, month+1, 0, 0, 0, 0, 0, time.UTC).Day()
對(duì)于Go中的time
標(biāo)準(zhǔn)庫(kù)包,正常月份的去值范圍為[1, 12]
,并且每個(gè)月的起始日是1
。 所以,y
年的m
月的起始時(shí)間就是time.Date(y, m, 1, 0, 0, 0, 0, time.UTC)
。
傳遞給time.Date
函數(shù)的實(shí)參可以超出它們的正常范圍,此函數(shù)將這些實(shí)參進(jìn)行規(guī)范化。 例如,1月32日會(huì)被轉(zhuǎn)換成2月1日。
以下是一些Go語(yǔ)言里的日期使用示例:
package main
import (
"time"
"fmt"
)
func main() {
// 2017-02-01 00:00:00 +0000 UTC
fmt.Println(time.Date(2017, 1, 32, 0, 0, 0, 0, time.UTC))
// 2017-01-31 23:59:59.999999999 +0000 UTC
fmt.Println(time.Date(2017, 1, 32, 0, 0, 0, -1, time.UTC))
// 2017-01-31 00:00:00 +0000 UTC
fmt.Println(time.Date(2017, 2, 0, 0, 0, 0, 0, time.UTC))
// 2016-12-31 00:00:00 +0000 UTC
fmt.Println(time.Date(2016, 13, 0, 0, 0, 0, 0, time.UTC))
// 2017-02-01 00:00:00 +0000 UTC
fmt.Println(time.Date(2016, 13, 32, 0, 0, 0, 0, time.UTC))
}
兩者都會(huì)將當(dāng)前的goroutine執(zhí)行暫停一段時(shí)間。 區(qū)別在于time.Sleep(d)
函數(shù)調(diào)用將使當(dāng)前的協(xié)程進(jìn)入睡眠子狀態(tài),但是當(dāng)前協(xié)程的(主)狀態(tài)依然為運(yùn)行狀態(tài); 而通道接收<-time.After(d)
操作將使當(dāng)前協(xié)程進(jìn)入阻塞狀態(tài)。
哈,我們不能保證這些函數(shù)的實(shí)現(xiàn)絕對(duì)沒(méi)有bug,但是如果這些函數(shù)返回的結(jié)果是不符你的預(yù)期,更有可能的是你的期望是不正確的。
標(biāo)準(zhǔn)包strings
和bytes
里有多個(gè)修剪(trim)函數(shù)。 這些函數(shù)可以被分類(lèi)為兩組:
Trim
、TrimLeft
、TrimRight
、TrimSpace
、TrimFunc
、TrimLeftFunc
和TrimRightFunc
。 這些函數(shù)將修剪首尾所有滿(mǎn)足指定(或隱含)條件的utf-8編碼的Unicode碼點(diǎn)(即rune)。(TrimSpace
隱含了修剪各種空格符。) 這些函數(shù)將檢查每個(gè)開(kāi)頭或結(jié)尾的rune值,直到遇到一個(gè)不滿(mǎn)足條件的rune值為止。TrimPrefix
和TrimSuffix
。 這兩個(gè)函數(shù)會(huì)把指定前綴或后綴的子字符串(或子切片)作為一個(gè)整體進(jìn)行修剪。 部分 程序員 會(huì)把TrimLeft
和TrimRight
函數(shù)當(dāng)作TrimPrefix
和TrimSuffix
函數(shù)而
誤用。 自然地,函數(shù)返回的結(jié)果很可能不是預(yù)期的那樣。
例如:
package main
import (
"fmt"
"strings"
)
func main() {
var s = "abaay森z眾xbbab"
o := fmt.Println
o(strings.TrimPrefix(s, "ab")) // aay森z眾xbbab
o(strings.TrimSuffix(s, "ab")) // abaay森z眾xbb
o(strings.TrimLeft(s, "ab")) // y森z眾xbbab
o(strings.TrimRight(s, "ab")) // abaay森z眾x
o(strings.Trim(s, "ab")) // y森z眾x
o(strings.TrimFunc(s, func(r rune) bool {
return r < 128 // trim all ascii chars
})) // 森z眾
}
fmt.Println
函數(shù)總會(huì)在兩個(gè)相鄰的參數(shù)之間輸出一個(gè)空格,然而fmt.Print
函數(shù)僅當(dāng)兩個(gè)相鄰的參數(shù)(的具體值)都不是字符串類(lèi)型時(shí)才會(huì)在它們之間輸出一個(gè)空格。
另外一個(gè)區(qū)別是fmt.Println
函數(shù)會(huì)在結(jié)尾寫(xiě)入一個(gè)換行符,但是fmt.Print
函數(shù)不會(huì)。
函數(shù)log.Print
與log.Println
的區(qū)別與上一個(gè)問(wèn)題里描述的關(guān)于函數(shù)fmt.Print
和fmt.Println
的第一個(gè)區(qū)別點(diǎn)類(lèi)似。
這兩個(gè)函數(shù)都會(huì)在結(jié)尾輸出一個(gè)換行符。
沒(méi)有。 如果有同步的需求,請(qǐng)使用log
標(biāo)準(zhǔn)庫(kù)包里的相應(yīng)函數(shù)。 你可以調(diào)用log.SetFlags(0)
來(lái)避免每一個(gè)日志行的前綴輸出。
除了上一個(gè)問(wèn)題里提到的區(qū)別之外,這三組函數(shù)之間還有一些其他區(qū)別。
print
/println
函數(shù)總是寫(xiě)入標(biāo)準(zhǔn)錯(cuò)誤。 fmt
標(biāo)準(zhǔn)包里的打印函數(shù)總是寫(xiě)入標(biāo)準(zhǔn)輸出。 log
標(biāo)準(zhǔn)包里的打印函數(shù)會(huì)默認(rèn)寫(xiě)入標(biāo)準(zhǔn)錯(cuò)誤,然而也可以通過(guò)log.SetOutput
函數(shù)來(lái)配置。print
/println
函數(shù)的調(diào)用不能接受數(shù)組和結(jié)構(gòu)體參數(shù)。print
/println
函數(shù)將輸出參數(shù)的底層值部的地址,而fmt
和log
標(biāo)準(zhǔn)庫(kù)包中的打印函數(shù)將輸出接口參數(shù)的動(dòng)態(tài)值的字面形式。print
/println
函數(shù)不會(huì)使調(diào)用參數(shù)引用的值逃逸到堆上,而fmt
和log
標(biāo)準(zhǔn)庫(kù)包中的打印函數(shù)將使調(diào)用參數(shù)引用的值逃逸到堆上。String() string
或Error() string
方法,那么fmt
和log
標(biāo)準(zhǔn)庫(kù)包里的打印函數(shù)在打印參數(shù)時(shí)會(huì)調(diào)用這兩個(gè)方法,而內(nèi)置的print
/println
函數(shù)則會(huì)忽略參數(shù)的這些方法。print
/println
函數(shù)不保證在未來(lái)的Go版本中繼續(xù)存在。通過(guò)math/rand
標(biāo)準(zhǔn)庫(kù)包生成的偽隨機(jī)數(shù)序列對(duì)于給定的種子是確定的。 這樣生成的隨機(jī)數(shù)不適用于安全敏感的環(huán)境中。 如果處于加密安全目的,我們應(yīng)該使用crypto/rand
標(biāo)準(zhǔn)庫(kù)包生成的偽隨機(jī)數(shù)序列。
math.Round
函數(shù)是有的,但是只是從Go 1.10開(kāi)始才有這個(gè)函數(shù)。 從Go 1.10開(kāi)始,標(biāo)準(zhǔn)庫(kù)添加了兩個(gè)新函數(shù)math.Round
和math.RoundToEven
。
在Go 1.10之前,關(guān)于 math.Round
函數(shù)是否應(yīng)該被添加進(jìn)標(biāo)準(zhǔn)包,經(jīng)歷了很長(zhǎng)時(shí)候的討論。
下列類(lèi)型不支持比較:
不支持比較的類(lèi)型不能用做映射類(lèi)型的鍵值類(lèi)型。
請(qǐng)注意:
nil
標(biāo)識(shí)符比較。關(guān)于為什么映射,切片和函數(shù)不支持比較,請(qǐng)閱讀Go的官方FAQ中關(guān)于這個(gè)問(wèn)答。
(Go官方FAQ中的這個(gè)答案也回答了這個(gè)問(wèn)題。)
一個(gè)接口值可以看作是一個(gè)包裹非接口值的盒子。被包裹在一個(gè)接口值中的非接口值的類(lèi)型必須實(shí)現(xiàn)了此接口值的類(lèi)型。 在Go中,很多種類(lèi)型的類(lèi)型的零值都是用nil
來(lái)表示的。 一個(gè)什么都沒(méi)包裹的接口值為一個(gè)零值接口值,即nil接口值。 一個(gè)包裹著其它非接口類(lèi)型的nil值的接口值并非什么都沒(méi)包裹,所以它不是(或者說(shuō)它不等于)一個(gè)nil接口值。
當(dāng)對(duì)一個(gè)nil接口值和一個(gè)nil非接口值進(jìn)行比較時(shí)(假設(shè)它們可以比較),此nil非接口值將先被轉(zhuǎn)換為nil接口值的類(lèi)型,然后再進(jìn)行比較; 此轉(zhuǎn)換的結(jié)果為一個(gè)包裹了此nil非接口值的一個(gè)副本的接口值,此接口值不是(或者說(shuō)它不等于)一個(gè)nil接口值,所以此比較不相等。
關(guān)于更詳細(xì)的解釋請(qǐng)閱讀接口和關(guān)于Go中的nil兩篇文章。
一個(gè)示例:
package main
import "fmt"
func main() {
var pi *int = nil
var pb *bool = nil
var x interface{} = pi
var y interface{} = pb
var z interface{} = nil
fmt.Println(x == y) // false
fmt.Println(x == nil) // false
fmt.Println(y == nil) // false
fmt.Println(x == z) // false
fmt.Println(y == z) // false
}
(不久前,Go官方FAQ也增加了一個(gè)相似的問(wèn)題。)
在Go語(yǔ)言中,僅當(dāng)兩個(gè)切片類(lèi)型共享相同的底層類(lèi)型時(shí),其中一個(gè)切片類(lèi)型才可以轉(zhuǎn)換成另一個(gè)切片的類(lèi)型而不需要使用unsafe
機(jī)制。
一個(gè)無(wú)名組合類(lèi)型的底層類(lèi)型是此組合類(lèi)型本身。 所以即便兩個(gè)不同的類(lèi)型T1
和T2
共享相同的底層類(lèi)型,類(lèi)型[]T1
和[]T2
也依然是不同的類(lèi)型,因此它們的底層類(lèi)型也是不同的。這意味著其中一個(gè)的值不能轉(zhuǎn)換為另一個(gè)。
底層類(lèi)型[]T1
和[]T2
不同的原因是:
[]T1
和[]T2
的值相互轉(zhuǎn)換的需求在實(shí)踐中并不常見(jiàn)。同樣的原因也適用于其它組合類(lèi)型。 例如:類(lèi)型map[T]T1
和 map[T]T2
同樣不共享相同的底層類(lèi)型,即便T1
和 T2
共享相同的底層類(lèi)型。
類(lèi)型[]T1
的值時(shí)候有可能通過(guò)使用unsafe
機(jī)制轉(zhuǎn)換成[]T2
的,但是一般不建議這么做:
package main
import (
"fmt"
"unsafe"
)
func main() {
type MyInt int
var a = []int{7, 8, 9}
var b = *(*[]MyInt)(unsafe.Pointer(&a))
b[0]= 123
fmt.Println(a) // [123 8 9]
fmt.Println(b) // [123 8 9]
fmt.Printf("%T \n", a) // []int
fmt.Printf("%T \n", b) // []main.MyInt
}
請(qǐng)注意:?
&T{}
?在Go里是一個(gè)語(yǔ)法糖,它是?tmp := T{}; (&tmp)
?的簡(jiǎn)寫(xiě)形式。 所以?&T{}
?是合法的并不代表字面量?T{}
?是可尋址的。
以下的值是可尋址的,因此可以被取地址:
在Go中,映射的設(shè)計(jì)保證一個(gè)映射值在內(nèi)存允許的情況下可以加入任意個(gè)條目。 另外為了防止一個(gè)映射中為其條目開(kāi)辟的內(nèi)存段支離破碎,官方標(biāo)準(zhǔn)編譯器使用了哈希表來(lái)實(shí)現(xiàn)映射。 并且為了保證元素索引的效率,一個(gè)映射值的底層哈希表只為其中的所有條目維護(hù)一段連續(xù)的內(nèi)存段。 因此,一個(gè)映射值隨著其中的條目數(shù)量逐漸增加時(shí),其維護(hù)的連續(xù)的內(nèi)存段需要不斷重新開(kāi)辟來(lái)增容,并把原來(lái)內(nèi)存段上的條目全部復(fù)制到新開(kāi)辟的內(nèi)存段上。 另外,即使一個(gè)映射值維護(hù)的內(nèi)存段沒(méi)有增容,某些哈希表實(shí)現(xiàn)也可能在當(dāng)前內(nèi)存段中移動(dòng)其中的條目。 總之,映射中的元素的地址會(huì)因?yàn)楦鞣N原因而改變。
如果映射元素可以被取地址,則Go運(yùn)行時(shí)(runtime)必須在元素地址改變的時(shí)候修改所有存儲(chǔ)了元素地址的指針值。 這極大得增加了Go編譯器和運(yùn)行時(shí)的實(shí)現(xiàn)難度,并且嚴(yán)重影響了程序運(yùn)行效率。 因此,目前,Go中禁止取映射元素的地址。
映射元素不可被取地址的另一個(gè)原因是表達(dá)式aMap[key]
可能返回一個(gè)存儲(chǔ)于aMap
中的元素,也可能返回一個(gè)不存儲(chǔ)于其中的元素零值。 這意味著表達(dá)式aMap[key]
在(&aMap[key]).Modify()
調(diào)用執(zhí)行之后可能仍然被估值為元素零值。 這將使很多人感到困惑,因此在Go中禁止取映射元素的地址。
切片的內(nèi)部類(lèi)型是一個(gè)結(jié)構(gòu)體,類(lèi)似于
struct {
elements unsafe.Pointer // 引用著一個(gè)元素序列
length int
capacity int
}
每一個(gè)切片間接引用一個(gè)元素序列。 盡管一個(gè)非空切片是不可取地址的,它的內(nèi)部元素序列需要開(kāi)辟在內(nèi)存中的某處因而必須是可取地址的。 取一個(gè)切片的元素地址事實(shí)上是取內(nèi)部元素序列上的元素地址。 因此,不可尋址的非空切片的元素也是可以被取地址的。
在Go語(yǔ)言中,為了方便,對(duì)于一個(gè)非指針和非接口類(lèi)型T
,
T
類(lèi)型的值可以調(diào)用為*T
類(lèi)型的方法,但是僅當(dāng)此T
的值是可尋址的情況下。
編譯器在調(diào)用指針屬主方法前,會(huì)自動(dòng)取此T
值的地址。
因?yàn)椴皇侨魏?code>T值都是可尋址的,所以并非任何T
值都能夠調(diào)用為類(lèi)型*T
的方法。
這種便利只是一個(gè)語(yǔ)法糖,而不是一種固有的規(guī)則。
*T
類(lèi)型的值可以調(diào)用為類(lèi)型T
的方法。
這是因?yàn)榻庖弥羔樋偸呛戏ǖ摹? 這種便利不僅僅是一個(gè)語(yǔ)法糖,它也是一種固有的規(guī)則。
所以很合理的, *T
的方法集總是T
方法集的超集,但反之不然。
事實(shí)上,你可以認(rèn)為對(duì)于每一個(gè)為類(lèi)型T
聲明的方法,編譯器都會(huì)為類(lèi)型*T
自動(dòng)隱式聲明一個(gè)同名和同簽名的方法。 詳見(jiàn)方法一文。
func (t T) MethodX(v0 ParamType0, ...) (ResultType0, ...) {
...
}
// 編譯器將會(huì)為*T隱式聲明一個(gè)如下的方法。
func (pt *T) MethodX(v0 ParamType0, ...) (ResultType0, ...) {
return (*pt).MethodX(v0, ...)
}
更多解釋請(qǐng)閱讀Go官方FAQ中的這個(gè)問(wèn)答。
請(qǐng)閱讀方法一文獲取答案。
如下是三種不可變值的定義:
在Go語(yǔ)言中,直到現(xiàn)在(Go 1.19),沒(méi)有值滿(mǎn)足第三種定義。
具名常量值滿(mǎn)足第一種定義。
方法和聲明的函數(shù)可以被視為聲明的不可變值。 它們滿(mǎn)足第二種定義。字符串的字節(jié)元素同樣滿(mǎn)足第二種定義。
在Go中沒(méi)有辦法聲明其它不可變值。
集合(set)可以看作是不關(guān)心元素值的映射。 在Go語(yǔ)言里,map[Tkey]struct{}
經(jīng)常被用做一個(gè)集合類(lèi)型。
在Go語(yǔ)言里,byte
是uint8
類(lèi)型的一個(gè)別名。 換言之,byte
和 uint8
是相同的類(lèi)型。 rune
和int32
屬于同樣類(lèi)似的關(guān)系。
一個(gè)rune
值通常被用來(lái)存儲(chǔ)一個(gè)Unicode碼點(diǎn)。
[]byte
和[]rune
類(lèi)型的值可以被顯式地直接轉(zhuǎn)換成字符串,反之亦然。
package main
import "fmt"
func main() {
var s0 = "Go"
var bs = []byte(s0)
var s1 = string(bs)
var rs = []rune(s0)
var s2 = string(rs)
fmt.Println(s0 == s1) // true
fmt.Println(s0 == s2) // true
}
更多關(guān)于字符串的信息,請(qǐng)閱讀Go中的字符串一文。
import (
"unsafe"
"sync/atomic"
)
type T int // just a demo
var p *T
func demo(newP *T) {
// 加載(讀?。? var _ = (*T)(atomic.LoadPointer(
(*unsafe.Pointer)(unsafe.Pointer(&p)),
))
// 存儲(chǔ)(修改)
atomic.StorePointer(
(*unsafe.Pointer)(unsafe.Pointer(&p)),
unsafe.Pointer(newP),
)
// 交換
var oldP = (*T)(atomic.SwapPointer(
(*unsafe.Pointer)(unsafe.Pointer(&p)),
unsafe.Pointer(newP),
))
// 比較并交換
var swapped = atomic.CompareAndSwapPointer(
(*unsafe.Pointer)(unsafe.Pointer(&p)),
unsafe.Pointer(oldP),
unsafe.Pointer(newP),
)
_ = swapped
}
是的,目前指針的原子操作使用起來(lái)非常得繁瑣。
Iota是希臘字母表中的第九個(gè)字母。 在Go語(yǔ)言中,iota
用在常量聲明中。 在每一個(gè)常量聲明組中,其值在該常量聲明組的第N個(gè)常量規(guī)范中的值為N
。
原因是此函數(shù)的實(shí)用性非常有限。 此類(lèi)函數(shù)調(diào)用的返回結(jié)果不能總是反映輸入通道實(shí)參的最新?tīng)顟B(tài)。 所以依靠此函數(shù)的返回結(jié)果來(lái)做決定不是一個(gè)好主意。
如果你確實(shí)需要這種函數(shù),你可以不怎么費(fèi)功夫地自己寫(xiě)一個(gè)。 請(qǐng)閱讀如何優(yōu)雅地關(guān)閉通道一文來(lái)了解如何編寫(xiě)一個(gè)closed
函數(shù)以及如何避免使用這樣的函數(shù)。
是的,在Go中這是絕對(duì)安全的。
支持棧的Go編譯器將會(huì)對(duì)每個(gè)局部變量進(jìn)行逃逸分析。 對(duì)于官方標(biāo)準(zhǔn)編譯器來(lái)說(shuō),如果一個(gè)值可以在編譯時(shí)刻被斷定它在運(yùn)行時(shí)刻僅會(huì)在一個(gè)協(xié)程中被使用,則此值將被開(kāi)辟在(此協(xié)程的)棧上;否則此值將被開(kāi)辟在堆上。 請(qǐng)閱讀內(nèi)存塊一文了解更多。
在Go社區(qū)中,gopher表示Go程序員。 這個(gè)昵稱(chēng)可能是源自于Go語(yǔ)言采用了一個(gè)卡通小地鼠(gopher)做為吉祥物。 順便說(shuō)一下,這個(gè)卡通小地鼠是由Renee French設(shè)計(jì)的。 Renee French是Go項(xiàng)目首任負(fù)責(zé)人Rob Pike的妻子。
更多建議: