Go語(yǔ)言 Go問(wèn)答

2023-02-16 17:41 更新

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

直到目前(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)題的打算。

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

在編寫(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,
}

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

對(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運(yùn)行時(shí)是否維護(hù)映射條目的遍歷順序?

不。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é)果中,映射條目總是排了序的。

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

至少對(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)體類(lèi)型的最后一個(gè)字段類(lèi)型的尺寸為零時(shí)會(huì)影響此結(jié)構(gòu)體的尺寸?

一個(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
}

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

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

運(yùn)行時(shí)錯(cuò)誤信息all goroutines are asleep - deadlock意味著什么?

用詞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í)令其崩潰退出。

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

傳遞給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)答以了解更多。

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

對(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)的Go編譯器是否支持函數(shù)內(nèi)聯(lián)?

是的,標(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)編譯器,

  • 沒(méi)有顯式的方式來(lái)在用戶(hù)代碼中指定哪些函數(shù)應(yīng)該被內(nèi)聯(lián)。
  • 盡管編譯參數(shù)-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)。 但是此方式不保證永久有效。

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

在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ù)來(lái)獲取任意月份的天數(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))
}

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

兩者都會(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)。

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

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

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

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

部分 程序員 會(huì)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ù)總會(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 和函數(shù) log.Println 有什么區(qū)別嗎?

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

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

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

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

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

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

  1. 內(nèi)置的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)配置。
  2. 內(nèi)置print/println函數(shù)的調(diào)用不能接受數(shù)組和結(jié)構(gòu)體參數(shù)。
  3. 對(duì)于組合類(lèi)型的參數(shù),內(nèi)置的print/println函數(shù)將輸出參數(shù)的底層值部的地址,而fmtlog標(biāo)準(zhǔn)庫(kù)包中的打印函數(shù)將輸出接口參數(shù)的動(dòng)態(tài)值的字面形式。
  4. 調(diào)用內(nèi)置的print/println函數(shù)不會(huì)使調(diào)用參數(shù)引用的值逃逸到堆上,而fmtlog標(biāo)準(zhǔn)庫(kù)包中的打印函數(shù)將使調(diào)用參數(shù)引用的值逃逸到堆上。
  5. 如果一個(gè)實(shí)參有String() stringError() string方法,那么fmtlog標(biāo)準(zhǔn)庫(kù)包里的打印函數(shù)在打印參數(shù)時(shí)會(huì)調(diào)用這兩個(gè)方法,而內(nèi)置的print/println函數(shù)則會(huì)忽略參數(shù)的這些方法。
  6. 內(nèi)置的print/println函數(shù)不保證在未來(lái)的Go版本中繼續(xù)存在。

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

通過(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ù)序列。

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

math.Round函數(shù)是有的,但是只是從Go 1.10開(kāi)始才有這個(gè)函數(shù)。 從Go 1.10開(kāi)始,標(biāo)準(zhǔn)庫(kù)添加了兩個(gè)新函數(shù)math.Roundmath.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)型不支持比較:

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

不支持比較的類(lèi)型不能用做映射類(lèi)型的鍵值類(lèi)型。

請(qǐng)注意:

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

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

為什么兩個(gè)nil值有時(shí)候會(huì)不相等?

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

為什么類(lèi)型[]T1和[]T2沒(méi)有共享相同底層類(lèi)型,即使不同的類(lèi)型T1和T2共享相同的底層類(lèi)型?

(不久前,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)型T1T2共享相同的底層類(lèi)型,類(lèi)型[]T1[]T2也依然是不同的類(lèi)型,因此它們的底層類(lèi)型也是不同的。這意味著其中一個(gè)的值不能轉(zhuǎn)換為另一個(gè)。

底層類(lèi)型[]T1[]T2不同的原因是:

同樣的原因也適用于其它組合類(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
}

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

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

請(qǐng)注意:?&T{}?在Go里是一個(gè)語(yǔ)法糖,它是?tmp := T{}; (&tmp)?的簡(jiǎn)寫(xiě)形式。 所以?&T{}?是合法的并不代表字面量?T{}?是可尋址的。

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

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

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

在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中禁止取映射元素的地址。

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

切片的內(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)部元素序列上的元素地址。 因此,不可尋址的非空切片的元素也是可以被取地址的。

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

在Go語(yǔ)言中,為了方便,對(duì)于一個(gè)非指針和非接口類(lèi)型T,

  • 一個(gè)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ī)則。
  • 一個(gè)*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)答

我們可以為哪些類(lèi)型聲明方法?

請(qǐng)閱讀方法一文獲取答案。

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

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

  1. 沒(méi)有地址的值(所以它們不可以尋址)。
  2. 有地址但是因?yàn)榉N種原因在語(yǔ)法上不可以尋址的值。
  3. 可尋址但不允許在語(yǔ)法上被修改的值。

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

具名常量值滿(mǎn)足第一種定義。

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

在Go中沒(méi)有辦法聲明其它不可變值。

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

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

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

在Go語(yǔ)言里,byteuint8類(lèi)型的一個(gè)別名。 換言之,byte 和 uint8是相同的類(lèi)型。 runeint32屬于同樣類(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是什么意思?

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

為什么沒(méi)有一個(gè)內(nèi)置的closed函數(shù)用來(lái)檢查通道是否已經(jīng)關(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ù)。

函數(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)存塊一文了解更多。

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

在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的妻子。


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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)