Go語言 Go問答

2023-02-16 17:41 更新

編譯器錯誤信息non-name *** on left side of :=意味著什么?

直到目前(Go 1.19), Go中對短變量聲明有一個強制性約束

所有位于?:=?符號左側(cè)的條目都必須是純標(biāo)識符,并且其中至少有一個為新變量名稱。

這意味著容器元素索引表達式(x[i])、結(jié)構(gòu)體的字段選擇器(x.f)、指針解引用(*p)和限定標(biāo)識符(aPackage.Value)都不能出現(xiàn)在:=符號的左側(cè)。

目前,這還是一個未解決問題(已經(jīng)和一個相關(guān)問題合并)。而且感覺Go核心開發(fā)團隊目前并未有立即解決此問題的打算。

編譯器錯誤信息unexpected newline, expecting { ...意味著什么?

在編寫Go代碼時,我們不能隨意斷行。 請閱讀代碼斷行規(guī)則一文以了解Go代碼斷行規(guī)則。 一般來說,根據(jù)這些規(guī)則,在左括號之前斷行是不合法的。

例如,下列代碼片段

if true
{
}

for i := 0; i < 10; i++
{
}

var _ = []int
{
	1, 2, 3
}

將會被編譯器解釋成

if true;
{
}

for i := 0; i < 10; i++;
{
}

var _ = []int;
{
	1, 2, 3;
}

Go編譯器將為每個左大括號{起始的代碼行報告一個語法錯誤。 為避免這些報錯,我們需要將上述代碼重寫為下面這樣:

if true {
}

for i := 0; i < 10; i++ {
}

var _ = []int {
	1, 2, 3,
}

編譯器錯誤信息declared and not used意味著什么?

對于標(biāo)準(zhǔn)編譯器,在局部代碼塊中聲明的每一個變量必須被至少一次用做r-value(right-hand-side value,右值)。

因此,下列代碼將編譯失敗,因為y只被用做目標(biāo)值(目標(biāo)值都為左值)。

func f(x bool) {
	var y = 1 // y被聲明了但沒有被用做右值
	if x {
		y = 2 // 這里,y被用做左值
	}
}

Go運行時是否維護映射條目的遍歷順序?

不。Go白皮書明確提到映射元素的迭代順序是未定義的。 所以對于同一個映射值,它的一個遍歷過程和下一個遍歷過程中的元素呈現(xiàn)次序不保證是相同的。 對于標(biāo)準(zhǔn)編譯器,映射元素的遍歷順序是隨機的。 如果你需要固定的映射元素遍歷順序,那么你就需要自己來維護這個順序。 更多信息請閱讀Go官方博客文章Go maps in action

但是請注意:從Go 1.12開始,標(biāo)準(zhǔn)庫包中的各個打印函數(shù)的結(jié)果中,映射條目總是排了序的。

Go編譯器是否會進行字節(jié)填充以確保結(jié)構(gòu)體字段的地址對齊?

至少對于標(biāo)準(zhǔn)的Go編譯器和gccgo,答案是肯定的。 具體需要填充多少個字節(jié)取決于操作系統(tǒng)和編譯器實現(xiàn)。 請閱讀關(guān)于Go值的內(nèi)存布局一文獲取詳情。

Go編譯器將不會重新排列結(jié)構(gòu)體的字段來最小化結(jié)構(gòu)體值的尺寸。 因為這樣做會導(dǎo)致意想不到的結(jié)果。 但是,根據(jù)需要,程序員可以手工重新排序字段來實現(xiàn)填充最小化。

為什么一個結(jié)構(gòu)體類型的最后一個字段類型的尺寸為零時會影響此結(jié)構(gòu)體的尺寸?

一個可尋址的結(jié)構(gòu)值的所有字段都可以被取地址。 如果非零尺寸的結(jié)構(gòu)體值的最后一個字段的尺寸是零,那么取此最后一個字段的地址將會返回一個越出了為此結(jié)構(gòu)體值分配的內(nèi)存塊的地址。 這個返回的地址可能指向另一個被分配的內(nèi)存塊。 在目前的官方Go標(biāo)準(zhǔn)運行時的實現(xiàn)中,如果一個內(nèi)存塊被至少一個依然活躍的指針引用,那么這個內(nèi)存塊將不會被視作垃圾因而肯定不會被回收。 所以只要有一個活躍的指針存儲著此非零尺寸的結(jié)構(gòu)體值的最后一個字段的越界地址,它將阻止垃圾收集器回收另一個內(nèi)存塊,從而可能導(dǎo)致內(nèi)存泄漏。

為避免上述問題,標(biāo)準(zhǔn)的Go編譯器會確保取一個非零尺寸的結(jié)構(gòu)體值的最后一個字段的地址時,絕對不會返回越出分配給此結(jié)構(gòu)體值的內(nèi)存塊的地址。 Go標(biāo)準(zhǔn)編譯器通過在需要時在結(jié)構(gòu)體最后的零尺寸字段之后填充一些字節(jié)來實現(xiàn)這一點。

如果一個結(jié)構(gòu)體的全部字段的類型都是零尺寸的(因此整個結(jié)構(gòu)體也是零尺寸的),那么就不需要再填充字節(jié),因為標(biāo)準(zhǔn)編譯器會專門處理零尺寸的內(nèi)存塊。

一個例子:

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
}

new(T)是var t T; (&t)的語法糖嗎?

雖然這兩者在實現(xiàn)上會有一些微妙的差別,取決于編譯器的具體實現(xiàn),但是我們基本上可以認(rèn)為這兩者是等價的。 即,通過new函數(shù)分配的內(nèi)存塊可以在棧上,也可以在堆上。

運行時錯誤信息all goroutines are asleep - deadlock意味著什么?

用詞asleep在這里其實并不準(zhǔn)確,實際上它的意思是處于阻塞狀態(tài)。

因為一個處于阻塞狀態(tài)的協(xié)程只能被另一個協(xié)程解除阻塞,如果程序中所有的協(xié)程都進入了阻塞狀態(tài),則它們將永遠都處于阻塞狀態(tài)。 這意味著程序死鎖了。一個正常運行的程序永遠不應(yīng)該死鎖,一個死鎖的程序肯定是由于邏輯實現(xiàn)上的bug造成的。 因此官方Go標(biāo)準(zhǔn)運行時將在一個程序死鎖時令其崩潰退出。

64位整數(shù)值的地址是否能保證總是64位對齊的,以便可以被安全地原子訪問?

傳遞給sync/atomic標(biāo)準(zhǔn)庫包中的64位函數(shù)的地址必須是64位對齊的,否則調(diào)用這些函數(shù)將在運行時導(dǎo)致恐慌產(chǎn)生。

對于標(biāo)準(zhǔn)編譯器和gccgo編譯器,在64位架構(gòu)下,64位整數(shù)的地址將保證總是64位對齊的。 所以它們總是可以被安全地原子訪問。 但在32位架構(gòu)下,64位整數(shù)的地址僅保證是32位對齊的。 所以原子訪問某些64位整數(shù)可能會導(dǎo)致恐慌。 但是,有一些方法可以保證一些64位整數(shù)總是可以被安全地原子訪問。 請閱讀關(guān)于Go值的內(nèi)存布局一文以獲得詳情。

賦值是原子操作嗎?

對于標(biāo)準(zhǔn)編譯器來說,賦值不是原子操作。

請閱讀官方FAQ中的此問答以了解更多。

是否每一個零值在內(nèi)存中占據(jù)的字節(jié)都是零?

對于大部分類型,答案是肯定的。不過事實上,這依賴于編譯器。 例如,對于標(biāo)準(zhǔn)編譯器,對于某些字符串類型的零值,此結(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
}

反過來,對于標(biāo)準(zhǔn)編譯器已經(jīng)支持的所有架構(gòu),如果一個值的所有字節(jié)都是零,那么這個值肯定是它的類型的零值。 然而,Go規(guī)范并沒有保證這一點。我曾聽說在某些比較老的處理器上,空指針表示的內(nèi)存地址并不為零。

標(biāo)準(zhǔn)的Go編譯器是否支持函數(shù)內(nèi)聯(lián)?

是的,標(biāo)準(zhǔn)編譯器支持函數(shù)內(nèi)聯(lián)。編譯器會自動內(nèi)聯(lián)一些滿足某些條件的短小函數(shù)。這些內(nèi)聯(lián)條件可能會在不同編譯器版本之間發(fā)生變化。

目前(Go 1.19),對于標(biāo)準(zhǔn)編譯器,

  • 沒有顯式的方式來在用戶代碼中指定哪些函數(shù)應(yīng)該被內(nèi)聯(lián)。
  • 盡管編譯參數(shù)-gcflags "-l"可以阻止任何函數(shù)被內(nèi)聯(lián), 但是并沒有一個正式的方式來避免某個特定的用戶函數(shù)被內(nèi)聯(lián)。 目前我們可以在函數(shù)聲明前增加一行//go:noinline 指令來避免這個函數(shù)被內(nèi)聯(lián)。 但是此方式不保證永久有效。

終結(jié)器(finalizer)可以用做對象的析構(gòu)函數(shù)嗎?

在Go程序里,我們可以通過調(diào)用runtime.SetFinalizer函數(shù)來給一個對象設(shè)置一個終結(jié)器函數(shù)。 一般說來,此終結(jié)器函數(shù)將在此對象被垃圾回收之前調(diào)用。 但是終結(jié)器并非被設(shè)計為對象的析構(gòu)函數(shù)。 通過runtime.SetFinalizer函數(shù)設(shè)置的終結(jié)器函數(shù)并不保證總會被運行。 因此我們不應(yīng)該依賴于終結(jié)器來保證程序的正確性。

終結(jié)器的主要用途是為了庫包的維護者能夠盡可能地避免因為庫包使用者不正確地使用庫包而帶來的危害。 例如,我們知道,當(dāng)在程序中使用完某個文件后,我們應(yīng)該將其關(guān)閉。 但是有時候因為種種原因,比如經(jīng)驗不足或者粗心大意,導(dǎo)致一些文件在使用完成后并未被關(guān)閉,那么和這些文件相關(guān)的很多資源只有在此程序退出之后才能得到釋放。這屬于資源泄漏。 為了盡可能地避免防止資源泄露,os庫包的維護者將會在一個os.File對象被被創(chuàng)建的時候為之設(shè)置一個終結(jié)器。 此終結(jié)器函數(shù)將關(guān)閉此os.File對象。當(dāng)此os.File對象因為不再被使用而被垃圾回收的時候,此終結(jié)器函數(shù)將被調(diào)用。

請記住,有一些終結(jié)器函數(shù)永遠不會被調(diào)用,并且有時候不當(dāng)?shù)脑O(shè)置終結(jié)器函數(shù)將會阻止對象被垃圾回收。 關(guān)于更多細節(jié),請閱讀runtime.SetFinalizer函數(shù)的文檔。

如何使用盡可能短的代碼行數(shù)來獲取任意月份的天數(shù)?

假設(shè)輸入的年份是一個自然年,并且輸入的月份也是一個自然月(1代表1月)。

days := time.Date(year, month+1, 0, 0, 0, 0, 0, time.UTC).Day()

對于Go中的time標(biāo)準(zhǔn)庫包,正常月份的去值范圍為[1, 12],并且每個月的起始日是1。 所以,y年的m月的起始時間就是time.Date(y, m, 1, 0, 0, 0, 0, time.UTC)。

傳遞給time.Date函數(shù)的實參可以超出它們的正常范圍,此函數(shù)將這些實參進行規(guī)范化。 例如,1月32日會被轉(zhuǎn)換成2月1日。

以下是一些Go語言里的日期使用示例:

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))
}

函數(shù)調(diào)用time.Sleep(d)和通道接收<-time.After(d)操作之間有何區(qū)別?

兩者都會將當(dāng)前的goroutine執(zhí)行暫停一段時間。 區(qū)別在于time.Sleep(d)函數(shù)調(diào)用將使當(dāng)前的協(xié)程進入睡眠子狀態(tài),但是當(dāng)前協(xié)程的(主)狀態(tài)依然為運行狀態(tài); 而通道接收<-time.After(d)操作將使當(dāng)前協(xié)程進入阻塞狀態(tài)。

調(diào)用strings和bytes標(biāo)準(zhǔn)庫包里TrimLeft和TrimRight函數(shù)經(jīng)常會返回不符預(yù)期的結(jié)果,這些函數(shù)的實現(xiàn)存在bugs嗎?

哈,我們不能保證這些函數(shù)的實現(xiàn)絕對沒有bug,但是如果這些函數(shù)返回的結(jié)果是不符你的預(yù)期,更有可能的是你的期望是不正確的。

標(biāo)準(zhǔn)包stringsbytes里有多個修剪(trim)函數(shù)。 這些函數(shù)可以被分類為兩組:

  1. Trim、TrimLeftTrimRight、TrimSpaceTrimFunc、TrimLeftFuncTrimRightFunc。 這些函數(shù)將修剪首尾所有滿足指定(或隱含)條件的utf-8編碼的Unicode碼點(即rune)。(TrimSpace隱含了修剪各種空格符。) 這些函數(shù)將檢查每個開頭或結(jié)尾的rune值,直到遇到一個不滿足條件的rune值為止。
  2. TrimPrefixTrimSuffix。 這兩個函數(shù)會把指定前綴或后綴的子字符串(或子切片)作為一個整體進行修剪。

部分 程序員 TrimLeftTrimRight函數(shù)當(dāng)作TrimPrefixTrimSuffix函數(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眾
}

函數(shù)fmt.Print和fmt.Println 的區(qū)別是什么?

fmt.Println函數(shù)總會在兩個相鄰的參數(shù)之間輸出一個空格,然而fmt.Print函數(shù)僅當(dāng)兩個相鄰的參數(shù)(的具體值)都不是字符串類型時才會在它們之間輸出一個空格。

另外一個區(qū)別是fmt.Println函數(shù)會在結(jié)尾寫入一個換行符,但是fmt.Print函數(shù)不會。

函數(shù)log.Print 和函數(shù) log.Println 有什么區(qū)別嗎?

函數(shù)log.Printlog.Println的區(qū)別與上一個問題里描述的關(guān)于函數(shù)fmt.Printfmt.Println的第一個區(qū)別點類似。

這兩個函數(shù)都會在結(jié)尾輸出一個換行符。

函數(shù)fmt.Print、fmt.Println和fmt.Printf的實現(xiàn)進行同步了嗎?

沒有。 如果有同步的需求,請使用log標(biāo)準(zhǔn)庫包里的相應(yīng)函數(shù)。 你可以調(diào)用log.SetFlags(0)來避免每一個日志行的前綴輸出。

內(nèi)置的print和println函數(shù)與fmt和log標(biāo)準(zhǔn)庫包中相應(yīng)的打印函數(shù)有什么區(qū)別?

除了上一個問題里提到的區(qū)別之外,這三組函數(shù)之間還有一些其他區(qū)別。

  1. 內(nèi)置的print/println函數(shù)總是寫入標(biāo)準(zhǔn)錯誤。 fmt標(biāo)準(zhǔn)包里的打印函數(shù)總是寫入標(biāo)準(zhǔn)輸出。 log標(biāo)準(zhǔn)包里的打印函數(shù)會默認(rèn)寫入標(biāo)準(zhǔn)錯誤,然而也可以通過log.SetOutput函數(shù)來配置。
  2. 內(nèi)置print/println函數(shù)的調(diào)用不能接受數(shù)組和結(jié)構(gòu)體參數(shù)。
  3. 對于組合類型的參數(shù),內(nèi)置的print/println函數(shù)將輸出參數(shù)的底層值部的地址,而fmtlog標(biāo)準(zhǔn)庫包中的打印函數(shù)將輸出接口參數(shù)的動態(tài)值的字面形式。
  4. 調(diào)用內(nèi)置的print/println函數(shù)不會使調(diào)用參數(shù)引用的值逃逸到堆上,而fmtlog標(biāo)準(zhǔn)庫包中的打印函數(shù)將使調(diào)用參數(shù)引用的值逃逸到堆上。
  5. 如果一個實參有String() stringError() string方法,那么fmtlog標(biāo)準(zhǔn)庫包里的打印函數(shù)在打印參數(shù)時會調(diào)用這兩個方法,而內(nèi)置的print/println函數(shù)則會忽略參數(shù)的這些方法。
  6. 內(nèi)置的print/println函數(shù)不保證在未來的Go版本中繼續(xù)存在。

標(biāo)準(zhǔn)庫包math/rand和crypto/rand生成的隨機數(shù)之間有什么區(qū)別?

通過math/rand標(biāo)準(zhǔn)庫包生成的偽隨機數(shù)序列對于給定的種子是確定的。 這樣生成的隨機數(shù)不適用于安全敏感的環(huán)境中。 如果處于加密安全目的,我們應(yīng)該使用crypto/rand標(biāo)準(zhǔn)庫包生成的偽隨機數(shù)序列。

標(biāo)準(zhǔn)庫中為什么沒有math.Round函數(shù)?

math.Round函數(shù)是有的,但是只是從Go 1.10開始才有這個函數(shù)。 從Go 1.10開始,標(biāo)準(zhǔn)庫添加了兩個新函數(shù)math.Roundmath.RoundToEven。

在Go 1.10之前,關(guān)于 math.Round函數(shù)是否應(yīng)該被添加進標(biāo)準(zhǔn)包,經(jīng)歷了很長時候的討論

哪些類型不支持比較?

下列類型不支持比較:

  • 映射(map)
  • 切片
  • 函數(shù)
  • 包含不可比較字段的結(jié)構(gòu)體類型
  • 元素類型為不可比較類型的數(shù)組類型

不支持比較的類型不能用做映射類型的鍵值類型。

請注意:

  • 盡管映射,切片和函數(shù)值不支持比較,但是它們的值可以與類型不確定的nil標(biāo)識符比較。
  • 如果兩個接口值的動態(tài)類型相同且不可比較,那么在運行時比較這兩個接口的值會產(chǎn)生一個恐慌。

關(guān)于為什么映射,切片和函數(shù)不支持比較,請閱讀Go的官方FAQ中關(guān)于這個問答。

為什么兩個nil值有時候會不相等?

(Go官方FAQ中的這個答案也回答了這個問題。)

一個接口值可以看作是一個包裹非接口值的盒子。被包裹在一個接口值中的非接口值的類型必須實現(xiàn)了此接口值的類型。 在Go中,很多種類型的類型的零值都是用nil來表示的。 一個什么都沒包裹的接口值為一個零值接口值,即nil接口值。 一個包裹著其它非接口類型的nil值的接口值并非什么都沒包裹,所以它不是(或者說它不等于)一個nil接口值。

當(dāng)對一個nil接口值和一個nil非接口值進行比較時(假設(shè)它們可以比較),此nil非接口值將先被轉(zhuǎn)換為nil接口值的類型,然后再進行比較; 此轉(zhuǎn)換的結(jié)果為一個包裹了此nil非接口值的一個副本的接口值,此接口值不是(或者說它不等于)一個nil接口值,所以此比較不相等。

關(guān)于更詳細的解釋請閱讀接口關(guān)于Go中的nil兩篇文章。

一個示例:

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
}

為什么類型[]T1和[]T2沒有共享相同底層類型,即使不同的類型T1和T2共享相同的底層類型?

(不久前,Go官方FAQ也增加了一個相似的問題。)

在Go語言中,僅當(dāng)兩個切片類型共享相同的底層類型時,其中一個切片類型才可以轉(zhuǎn)換成另一個切片的類型而不需要使用unsafe機制

一個無名組合類型的底層類型是此組合類型本身。 所以即便兩個不同的類型T1T2共享相同的底層類型,類型[]T1[]T2也依然是不同的類型,因此它們的底層類型也是不同的。這意味著其中一個的值不能轉(zhuǎn)換為另一個。

底層類型[]T1[]T2不同的原因是:

同樣的原因也適用于其它組合類型。 例如:類型map[T]T1 和 map[T]T2同樣不共享相同的底層類型,即便T1 和 T2共享相同的底層類型。

類型[]T1的值時候有可能通過使用unsafe機制轉(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
}

哪些值可以被取地址,哪些值不可以被取地址?

  • 字符串的字節(jié)元素
  • 映射元素
  • 接口值的動態(tài)值(類型斷言的結(jié)果)
  • 常量(包括具名常量和字面量)
  • 聲明的包級別函數(shù)
  • 方法(用做函數(shù)值)
  • 中間結(jié)果值
    • 函數(shù)調(diào)用
    • 顯式值轉(zhuǎn)換
    • 各種操作,不包含指針解引用(dereference)操作,但是包含:
      • 通道接收操作
      • 子字符串操作
      • 子切片操作
      • 加法、減法、乘法、以及除法等等。

請注意:?&T{}?在Go里是一個語法糖,它是?tmp := T{}; (&tmp)?的簡寫形式。 所以?&T{}?是合法的并不代表字面量?T{}?是可尋址的。

以下的值是可尋址的,因此可以被取地址:

  • 變量
  • 可尋址的結(jié)構(gòu)體的字段
  • 可尋址的數(shù)組的元素
  • 任意切片的元素(無論是可尋址切片或不可尋址切片)
  • 指針解引用(dereference)操作

為什么映射元素不可被取地址?

在Go中,映射的設(shè)計保證一個映射值在內(nèi)存允許的情況下可以加入任意個條目。 另外為了防止一個映射中為其條目開辟的內(nèi)存段支離破碎,官方標(biāo)準(zhǔn)編譯器使用了哈希表來實現(xiàn)映射。 并且為了保證元素索引的效率,一個映射值的底層哈希表只為其中的所有條目維護一段連續(xù)的內(nèi)存段。 因此,一個映射值隨著其中的條目數(shù)量逐漸增加時,其維護的連續(xù)的內(nèi)存段需要不斷重新開辟來增容,并把原來內(nèi)存段上的條目全部復(fù)制到新開辟的內(nèi)存段上。 另外,即使一個映射值維護的內(nèi)存段沒有增容,某些哈希表實現(xiàn)也可能在當(dāng)前內(nèi)存段中移動其中的條目。 總之,映射中的元素的地址會因為各種原因而改變。 如果映射元素可以被取地址,則Go運行時(runtime)必須在元素地址改變的時候修改所有存儲了元素地址的指針值。 這極大得增加了Go編譯器和運行時的實現(xiàn)難度,并且嚴(yán)重影響了程序運行效率。 因此,目前,Go中禁止取映射元素的地址。

映射元素不可被取地址的另一個原因是表達式aMap[key]可能返回一個存儲于aMap中的元素,也可能返回一個不存儲于其中的元素零值。 這意味著表達式aMap[key](&aMap[key]).Modify()調(diào)用執(zhí)行之后可能仍然被估值為元素零值。 這將使很多人感到困惑,因此在Go中禁止取映射元素的地址。

為什么非空切片的元素總是可被取地址,即便對于不可尋址的切片也是如此?

切片的內(nèi)部類型是一個結(jié)構(gòu)體,類似于

struct {
	elements unsafe.Pointer // 引用著一個元素序列
	length   int
	capacity int
}

每一個切片間接引用一個元素序列。 盡管一個非空切片是不可取地址的,它的內(nèi)部元素序列需要開辟在內(nèi)存中的某處因而必須是可取地址的。 取一個切片的元素地址事實上是取內(nèi)部元素序列上的元素地址。 因此,不可尋址的非空切片的元素也是可以被取地址的。

對任意的非指針和非接口類型T,為什么類型*T的方法集總是類型T的方法集的超集,但是反之卻不然?

在Go語言中,為了方便,對于一個非指針和非接口類型T

  • 一個T類型的值可以調(diào)用為*T類型的方法,但是僅當(dāng)此T的值是可尋址的情況下。 編譯器在調(diào)用指針屬主方法前,會自動取此T值的地址。 因為不是任何T值都是可尋址的,所以并非任何T值都能夠調(diào)用為類型*T的方法。 這種便利只是一個語法糖,而不是一種固有的規(guī)則。
  • 一個*T類型的值可以調(diào)用為類型T的方法。 這是因為解引用指針總是合法的。 這種便利不僅僅是一個語法糖,它也是一種固有的規(guī)則。

所以很合理的, *T的方法集總是T方法集的超集,但反之不然。

事實上,你可以認(rèn)為對于每一個為類型T聲明的方法,編譯器都會為類型*T自動隱式聲明一個同名和同簽名的方法。 詳見方法一文。

func (t T) MethodX(v0 ParamType0, ...) (ResultType0, ...) {
	...
}

// 編譯器將會為*T隱式聲明一個如下的方法。
func (pt *T) MethodX(v0 ParamType0, ...) (ResultType0, ...) {
	return (*pt).MethodX(v0, ...)
}

更多解釋請閱讀Go官方FAQ中的這個問答

我們可以為哪些類型聲明方法?

請閱讀方法一文獲取答案。

在Go里如何聲明不可變量?

如下是三種不可變值的定義:

  1. 沒有地址的值(所以它們不可以尋址)。
  2. 有地址但是因為種種原因在語法上不可以尋址的值。
  3. 可尋址但不允許在語法上被修改的值。

在Go語言中,直到現(xiàn)在(Go 1.19),沒有值滿足第三種定義。

具名常量值滿足第一種定義。

方法和聲明的函數(shù)可以被視為聲明的不可變值。 它們滿足第二種定義。字符串的字節(jié)元素同樣滿足第二種定義。

在Go中沒有辦法聲明其它不可變值。

為什么沒有內(nèi)置的set容器類型?

集合(set)可以看作是不關(guān)心元素值的映射。 在Go語言里,map[Tkey]struct{}經(jīng)常被用做一個集合類型。

什么是byte?什么是rune? 如何將[]byte和[]rune類型的值轉(zhuǎn)換為字符串?

在Go語言里,byteuint8類型的一個別名。 換言之,byte 和 uint8是相同的類型。 runeint32屬于同樣類似的關(guān)系。

一個rune值通常被用來存儲一個Unicode碼點。

[]byte[]rune類型的值可以被顯式地直接轉(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)于字符串的信息,請閱讀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)),
		))

	// 存儲(修改)
	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
}

是的,目前指針的原子操作使用起來非常得繁瑣。

iota是什么意思?

Iota是希臘字母表中的第九個字母。 在Go語言中,iota用在常量聲明中。 在每一個常量聲明組中,其值在該常量聲明組的第N個常量規(guī)范中的值為N。

為什么沒有一個內(nèi)置的closed函數(shù)用來檢查通道是否已經(jīng)關(guān)閉?

原因是此函數(shù)的實用性非常有限。 此類函數(shù)調(diào)用的返回結(jié)果不能總是反映輸入通道實參的最新狀態(tài)。 所以依靠此函數(shù)的返回結(jié)果來做決定不是一個好主意。

如果你確實需要這種函數(shù),你可以不怎么費功夫地自己寫一個。 請閱讀如何優(yōu)雅地關(guān)閉通道一文來了解如何編寫一個closed函數(shù)以及如何避免使用這樣的函數(shù)。

函數(shù)返回局部變量的指針是否安全?

是的,在Go中這是絕對安全的。

支持棧的Go編譯器將會對每個局部變量進行逃逸分析。 對于官方標(biāo)準(zhǔn)編譯器來說,如果一個值可以在編譯時刻被斷定它在運行時刻僅會在一個協(xié)程中被使用,則此值將被開辟在(此協(xié)程的)棧上;否則此值將被開辟在堆上。 請閱讀內(nèi)存塊一文了解更多。

單詞gopher在Go社區(qū)中表示什么?

在Go社區(qū)中,gopher表示Go程序員。 這個昵稱可能是源自于Go語言采用了一個卡通小地鼠(gopher)做為吉祥物。 順便說一下,這個卡通小地鼠是由Renee French設(shè)計的。 Renee French是Go項目首任負責(zé)人Rob Pike的妻子。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號