Go語言 Go細(xì)節(jié)

2023-02-16 17:41 更新

一個(gè)包可以在一個(gè)源文件里被引入多次。

一個(gè)Go源文件可以多次引入同一個(gè)包。但是每次的引入名稱必須不同。這些相同的包引入引用著同一個(gè)包實(shí)例。

示例:

package main

import "fmt"
import "io"
import inout "io"

func main() {
	fmt.Println(&inout.EOF == &io.EOF) // true
}

在switch和select流程控制代碼塊中,default分支可以放在所有的case分支之前或者所有的case分支之后,也可以放在case分支之間。

示例:

	switch n := rand.Intn(3); n {
	case 0: fmt.Println("n == 0")
	case 1: fmt.Println("n == 1")
	default: fmt.Println("n == 2")
	}

	switch n := rand.Intn(3); n {
	default: fmt.Println("n == 2")
	case 0: fmt.Println("n == 0")
	case 1: fmt.Println("n == 1")
	}

	switch n := rand.Intn(3); n {
	case 0: fmt.Println("n == 0")
	default: fmt.Println("n == 2")
	case 1: fmt.Println("n == 1")
	}

	var x, y chan int

	select {
	case <-x:
	case y <- 1:
	default:
	}

	select {
	case <-x:
	default:
	case y <- 1:
	}

	select {
	default:
	case <-x:
	case y <- 1:
	}

switch流程控制代碼塊中的數(shù)字常量case表達(dá)式不能重復(fù),但是布爾常量case表達(dá)式可以重復(fù)。

例如,下面的代碼在編譯時(shí)會(huì)失敗。

package main

func main() {
	switch 123 {
	case 123:
	case 123: // error: duplicate case
	}
}

但是下面的代碼在編譯時(shí)是沒問題的。

package main

func main() {
	switch false {
	case false:
	case false:
	}
}

關(guān)于原因,請閱讀這個(gè)issue。 此行為依賴于編譯器。事實(shí)上,標(biāo)準(zhǔn)編譯器同樣不允許重復(fù)的字符串case表達(dá)式,但是gccgo編譯器卻允許。

switch流程控制代碼塊里的switch表達(dá)式總是被估值為類型確定值。

例如,在下列switch代碼塊中的switch表達(dá)式123被視為一個(gè)int值,而不是一個(gè)類型不確定的整數(shù)。

package main

func main() {
	switch 123 {
	case int64(123):  // error: 類型不匹配
	case uint32(789): // error: 類型不匹配
	}
}

switch流程控制代碼塊中的switch表達(dá)式的缺省默認(rèn)值為類型確定值true(其類型為預(yù)聲明類型bool)。

例如,下列程序會(huì)打印出true。

package main

import "fmt"

func main() {
	switch { // <=> switch true {
	case true:  fmt.Println("true")
	case false: fmt.Println("false")
	}
}

有時(shí)候,顯式代碼塊的開括號{可以放在下一行。

例如:

package main

func main() {
	var i = 0
Outer:
	for
	{ // 在這里斷行是沒問題的
		switch
		{ // 在這里斷行是沒問題的
		case i == 5:
			break Outer
		default:
			i++
		}
	}
}

下面程序的結(jié)果會(huì)打印什么?true還是false? 答案是true。 關(guān)于原因請閱讀Go中的代碼斷行規(guī)則一文。

package main

import "fmt"

func False() bool {
	return false
}

func main() {
	switch False()
	{
	case true:  fmt.Println("true")
	case false: fmt.Println("false")
	}
}

有些case分支代碼塊必須是顯式的。

例如,下面的程序會(huì)在編譯時(shí)將失敗。

func demo(n, m int) (r int) {
	switch n {
	case 123:
		if m > 0 {
			goto End
		}
		r++

		End: // syntax error: 標(biāo)簽后缺少語句
	default:
		r = 1
	}
	return
}

為了編譯通過,case分支代碼塊必須改成顯式的:

func demo(n, m int) (r int) {
	switch n {
	case 123: {
		if m > 0 {
			goto End
		}
		r++

		End:
	}
	default:
		r = 1
	}
	return
}

另外,我們可以在標(biāo)簽End:之后加一個(gè)分號,如下所示:

func demo(n, m int) (r int) {
	switch n {
	case 123:
		if m > 0 {
			goto End
		}
		r++

		End:;
	default:
		r = 1
	}
	return
}

關(guān)于原因,請閱讀Go的代碼斷行規(guī)則一文。

嵌套的延遲函數(shù)調(diào)用可以修改外層函數(shù)的返回結(jié)果。

例如:

package main

import "fmt"

func F() (r int) {
	defer func() {
		r = 789
	}()

	return 123 // <=> r = 123; return
}

func main() {
	fmt.Println(F()) // 789
}

某些recover函數(shù)調(diào)用是空操作。

我們需要在正確的地方調(diào)用recover函數(shù)。 關(guān)于細(xì)節(jié),請閱讀 在正確的位置調(diào)用內(nèi)置函數(shù)recover一文。

我們可以使用os.Exit函數(shù)調(diào)用退出一個(gè)程序和使用runtime.Goexit函數(shù)調(diào)用退出一個(gè)協(xié)程。

我們可以通過調(diào)用os.Exit函數(shù)從任何函數(shù)里退出一個(gè)程序。 os.Exit函數(shù)調(diào)用接受一個(gè)int代碼值做為參數(shù)并將此代碼返回給操作系統(tǒng)。

示例:

// exit-example.go
package main

import "os"
import "time"

func main() {
	go func() {
		time.Sleep(time.Second)
		os.Exit(1)
	}()
	select{}
}

運(yùn)行:

$ go run a.go
exit status 1
$ echo $?
1

我們可以通過調(diào)用runtime.Goexit函數(shù)退出一個(gè)goroutine。 runtime.Goexit函數(shù)沒有參數(shù)。

在下面的示例中,文字Java將不會(huì)被打印出來。

package main

import "fmt"
import "runtime"

func main() {
	c := make(chan int)
	go func() {
		defer func() {c <- 1}()
		defer fmt.Println("Go")
		func() {
			defer fmt.Println("C")
			runtime.Goexit()
		}()
		fmt.Println("Java")
	}()
	<-c
}

遞增運(yùn)算符++和遞減運(yùn)算符--的優(yōu)先級低于解引用運(yùn)算符*和取地址運(yùn)算符&,解引用運(yùn)算符和取地址運(yùn)算符的優(yōu)先級低于選擇器.中的屬性選擇操作符。

例如:

package main

import "fmt"

type T struct {
	x int
	y *int
}

func main() {
	var t T
	p := &t.x // <=> p := &(t.x)
	fmt.Printf("%T\n", p) // *int

	*p++ // <=> (*p)++
	*p-- // <=> (*p)--

	t.y = p
	a := *t.y // <=> *(t.y)
	fmt.Printf("%T\n", a) // int
}

移位運(yùn)算中的左類型不確定操作數(shù)的類型推斷規(guī)則取決于右操作數(shù)是否是常量。

package main

func main() {
}

const M  = 2
var _ = 1.0 << M // 編譯沒問題。1.0將被推斷為一個(gè)int值。

var N = 2
var _ = 1.0 << N // 編譯失敗。1.0將被推斷為一個(gè)float64值。

關(guān)于原因請閱讀運(yùn)算操作符一文。

如果兩個(gè)指針的類型具有不同的底層類型但是它們的基類型卻共享相同的底層類型,則這兩個(gè)指針值可以間接相互轉(zhuǎn)換為對方的類型。

例如:

package main

type MyInt int64
type Ta    *int64
type Tb    *MyInt

func main() {
	var a Ta
	var b Tb

	//a = Ta(b) // error: 直接轉(zhuǎn)換是不允許的。

	// 但是間接轉(zhuǎn)換是允許的。
	y := (*MyInt)(b)
	x := (*int64)(y)
	a = x           // 等價(jià)于下一行
	a = (*int64)(y) // 等價(jià)于下一行
	a = (*int64)((*MyInt)(b))
	_ = a
}

兩個(gè)零尺寸值的地址可能相等,也可能不相等。

兩個(gè)零尺寸值的地址是否相等時(shí)依賴于具體編譯器實(shí)現(xiàn)以及具體編譯器版本。

package main

import "fmt"

func main() {
	a := struct{}{}
	b := struct{}{}
	x := struct{}{}
	y := struct{}{}
	m := [10]struct{}{}
	n := [10]struct{}{}
	o := [10]struct{}{}
	p := [10]struct{}{}

	fmt.Println(&x, &y, &o, &p)

	// 對于標(biāo)準(zhǔn)編譯器1.19版本,x、y、o和p將
	// 逃逸到堆上,但是a、b、m和n則開辟在棧上。

	fmt.Println(&a == &b) // false
	fmt.Println(&x == &y) // true
	fmt.Println(&a == &x) // false

	fmt.Println(&m == &n) // false
	fmt.Println(&o == &p) // true
	fmt.Println(&n == &p) // false
}

上面代碼中所示的輸出是針對標(biāo)準(zhǔn)編譯器1.19版本的。

一個(gè)指針類型的基類型可以為此指針類型自身。

例如:

package main

func main() {
	type P *P
	var p P
	p = &p
	p = **************p
}

類似的,

  • 一個(gè)切片類型的元素類型可以是此切片類型自身,
  • 一個(gè)映射類型的元素類型可以是此映射類型自身,
  • 一個(gè)通道類型的元素類型可以是此通道類型自身,
  • 一個(gè)函數(shù)類型的輸入?yún)?shù)和返回結(jié)果值類型可以是此函數(shù)類型自身。
package main

func main() {
	type S []S
	type M map[string]M
	type C chan C
	type F func(F) F

	s := S{0:nil}
	s[0] = s
	m := M{"Go": nil}
	m["Go"] = m
	c := make(C, 3)
	c <- c; c <- c; c <- c
	var f F
	f = func(F)F {return f}

	_ = s[0][0][0][0][0][0][0][0]
	_ = m["Go"]["Go"]["Go"]["Go"]
	<-<-<-c
	f(f(f(f(f))))
}

有關(guān)選擇器縮寫形式的細(xì)節(jié)。

無論一個(gè)指針值的類型是具名的還是無名的,如果它的(指針)類型的基類型為一個(gè)結(jié)構(gòu)體類型,則我們可以使用此指針值來選擇它所引用著的結(jié)構(gòu)體中的字段。 但是,如果此指針的類型為一個(gè)具名類型,則我們不能使用此指針值來選擇它所引用著的結(jié)構(gòu)體中的方法。

我們總是不能使用二級以上指針來選擇結(jié)構(gòu)體字段和方法。

package main

type T struct {
	x int
}
func (T) m(){} // T有一個(gè)方法m。

type P *T  // P為一個(gè)具名一級指針類型。
type PP *P // PP為一個(gè)具名二級指針類型。

func main() {
	var t T
	var tp = &t
	var tpp = &tp
	var p P = tp
	var pp PP = &p
	tp.x = 12  // 沒問題
	p.x = 34   // 沒問題
	pp.x = 56  // error: 類型PP沒有名為x的字段或者方法。
	tpp.x = 78 // error: 類型**T沒有名為x的字段或者方法。

	tp.m()  // 沒問題,因?yàn)轭愋?T也有一個(gè)m方法。
	p.m()   // error: 類型P沒有名為m的字段或者方法。
	pp.m()  // error: 類型PP沒有名為m的字段或者方法。
	tpp.m() // error: 類型**T沒有名為m的字段或者方法。
}

有時(shí)候,嵌套組合字面量可以被簡化。

關(guān)于細(xì)節(jié),請閱讀內(nèi)嵌組合字面量可以被簡化這一章節(jié)。

在某些情形下,我們可以將數(shù)組指針當(dāng)作數(shù)組來用。

關(guān)于細(xì)節(jié),請閱讀把數(shù)組指針當(dāng)做數(shù)組來使用這一章節(jié)。

從nil映射中讀取元素不會(huì)導(dǎo)致崩潰,讀取結(jié)果是一個(gè)零元素值。

例如,函數(shù)Foo1Foo2是等價(jià)的,但是函數(shù)Foo2比函數(shù)Foo1簡潔得多。

func Foo1(m map[string]int) int {
	if m != nil {
		return m["foo"]
	}
	return 0
}

func Foo2(m map[string]int) int {
	return m["foo"]
}

從一個(gè)nil映射中刪除一個(gè)條目不會(huì)導(dǎo)致崩潰,這是一個(gè)空操作。

例如,下面這個(gè)程序不會(huì)因?yàn)榭只哦罎ⅰ?

package main

func main() {
	var m map[string]int // nil
	delete(m, "foo")
}

append函數(shù)調(diào)用的結(jié)果可能會(huì)與原始切片共享一些元素,也可能不共享任何元素。

關(guān)于細(xì)節(jié),請閱讀添加和刪除容器元素這一章節(jié)。

從一個(gè)基礎(chǔ)切片派生出的子切片的長度可能大于基礎(chǔ)切片的長度。

例如:

package main

import "fmt"

func main() {
	s := make([]int, 3, 9)
	fmt.Println(len(s)) // 3
	s2 := s[2:7]
	fmt.Println(len(s2)) // 5
}

關(guān)于細(xì)節(jié),請閱讀從數(shù)組或者切片派生切片這一章節(jié)。

從一個(gè)nil切片中派生子切片是允許的,只要子切片表達(dá)式中使用的所有索引都為零,則不會(huì)有恐慌產(chǎn)生,結(jié)果子切片同樣是一個(gè)nil切片。

例如,下面的程序在運(yùn)行時(shí)刻不會(huì)產(chǎn)生恐慌。

package main

import "fmt"

func main() {
	var x []int // nil
	a := x[:]
	b := x[0:0]
	c := x[:0:0]
	// 下一行將打印出三個(gè)true。
	fmt.Println(a == nil, b == nil, c == nil)
}

關(guān)于細(xì)節(jié),請閱讀從數(shù)組或者切片派生切片這一章節(jié)。

用range遍歷nil映射或者nil切片是沒問題的,這屬于空操作。

例如,下面的程序可以編譯是沒問題的。

package main

func main() {
	var s []int // nil
	for range s {
	}

	var m map[string]int // nil
	for range m {
	}
}

用range遍歷nil數(shù)組指針時(shí),如果忽略或省略第二個(gè)迭代變量,則此遍歷是沒問題的。遍歷中的循環(huán)步數(shù)為相應(yīng)數(shù)組類型的長度。

例如,下面的程序會(huì)輸出01234。

package main

import "fmt"

func main() {
	var a *[5]int // nil
	for i, _ := range a {
		fmt.Print(i)
	}
}

切片的長度和容量可以被單獨(dú)修改。

我們可以通過反射途徑單獨(dú)修改一個(gè)切片的長度或者容量。 關(guān)于細(xì)節(jié),請閱讀單獨(dú)修改一個(gè)切片的長度或者容量這一章節(jié)。

切片和數(shù)組組合字面量中的索引必須是非負(fù)常量。

例如,下面的程序?qū)⒕幾g失敗。

var k = 1
var x = [2]int{k: 1} // error: 索引必須為一個(gè)常量
var y = []int{k: 1}  // error: 索引必須為一個(gè)常量

注意,映射組合字面量中的鍵值不必為常量。

切片/數(shù)組/映射組合字面量的常量索引和鍵值不能重復(fù)。

例如,下面的程序?qū)⒕幾g失敗。

// error: 重復(fù)的索引:1
var a = []bool{0: false, 1: true, 1: true}
// error: 重復(fù)的索引:0
var b = [...]string{0: "foo", 1: "bar", 0: "foo"}
// error: 重復(fù)的鍵值:"foo"
var c = map[string]int{"foo": 1, "foo": 2}

這個(gè)特性可以用于在編譯時(shí)刻斷言某些條件。

不可尋址的數(shù)組的元素依舊是不可尋址的,但是不可尋址的切片的元素總是可尋址的。

原因是一個(gè)數(shù)組值的元素和此數(shù)組存儲(chǔ)在同一個(gè)內(nèi)存塊中。 但是切片的情況大不相同。

一個(gè)例子:

package main

func main() {
	// 組合字面量是不可尋址的。

	/* 取容器元素的地址。 */

	// 取不可尋址的切片的元素的地址是沒問題的
	_ = &[]int{1}[0]
	// error: 不能取不可尋址的數(shù)組的元素的地址
	_ = &[5]int{}[0]

	/* 修改元素值。 */

	// 修改不可尋址的切片的元素是沒問題的
	[]int{1,2,3}[1] = 9
	// error: 不能修改不可尋址的數(shù)組的元素
	[3]int{1,2,3}[1] = 9
}

可以從不可尋址的切片派生子切片,但是不能從不可尋址的數(shù)組派生子切片。

原因和上一個(gè)細(xì)節(jié)是一樣的。

例如:

package main

func main() {
	// 映射元素是不可尋址的。

	// 下面幾行編譯沒問題。
	_ = []int{6, 7, 8, 9}[1:3]
	var ms = map[string][]int{"abc": {0, 1, 2, 3}}
	_ = ms["abc"][1:3]

	// 下面幾行將編譯失敗,因?yàn)椴豢蓮牟豢蓪ぶ返臄?shù)組派生切片。
	/*
	_ = [...]int{6, 7, 8, 9}[1:3] // error
	var ma = map[string][4]int{"abc": {0, 1, 2, 3}}
	_ = ma["abc"][1:3]  // error
	*/
}

把以NaN做為鍵值的條目放如映射就宛如把條目放入黑洞一樣。

原因是下面的另一個(gè)細(xì)節(jié)中提到的NaN != NaN。 但是,在Go1.12之前,以NaN作為鍵值的元素只能在for-range循環(huán)中被找到; 從Go1.12開始,以NaN作為鍵值的元素也可以通過類似fmt.Print的函數(shù)打印出來。

package main

import "fmt"
import "math"

func main() {
	var a = math.NaN()
	fmt.Println(a) // NaN

	var m = map[float64]int{}
	m[a] = 123
	v, present := m[a]
	fmt.Println(v, present) // 0 false
	m[a] = 789
	v, present = m[a]
	fmt.Println(v, present) // 0 false

	fmt.Println(m) // map[NaN:789 NaN:123]
	delete(m, a)   // no-op
	fmt.Println(m) // map[NaN:789 NaN:123]

	for k, v := range m {
		fmt.Println(k, v)
	}
	// the above loop outputs:
	// NaN 123
	// NaN 789
}

注意:在Go1.12之前,兩個(gè)fmt.Println(m)調(diào)用均打印出map[NaN:<nil> NaN:<nil>]。

字符串轉(zhuǎn)換為byte切片或rune切片后的結(jié)果切片的容量可能會(huì)大于長度。

我們不應(yīng)該假設(shè)結(jié)果切片的長度和容量總是相等的。

在下面的例子中,如果最后一個(gè)fmt.Println行被刪除,在其前面的兩行會(huì)打印相同的值32;否則,一個(gè)打印32,一個(gè)打印8(對于標(biāo)準(zhǔn)編譯器1.19版本來說)。

package main

import "fmt"

func main() {
	s := "a"
	x := []byte(s)              // len(s) == 1
	fmt.Println(cap([]byte(s))) // 32
	fmt.Println(cap(x))         // 8
	fmt.Println(x)
}

如果我們假設(shè)結(jié)果切片的長度和容量總是相等,就可能寫出一些有bug的代碼

對于切片s,循環(huán)for i = range s {...}并不等價(jià)于循環(huán)for i = 0; i < len(s); i++ {...}。

對于這兩個(gè)循環(huán),迭代變量i的最終值可能是不同的。

package main

import "fmt"

var i int

func fa(s []int, n int) int {
	i = n
	for i = 0; i < len(s); i++ {}
	return i
}

func fb(s []int, n int) int {
	i = n
	for i = range s {}
	return i
}

func main() {
	s := []int{2, 3, 5, 7, 11, 13}
	fmt.Println(fa(s, -1), fb(s, -1)) // 6 5
	s = nil
	fmt.Println(fa(s, -1), fb(s, -1)) // 0 -1
}

一個(gè)映射中的條目的遍歷次序在兩次遍歷中可能并不相同。我們可以認(rèn)為映射中的條目的遍歷次序是隨機(jī)的。

比如下面這個(gè)例子不會(huì)無窮盡地循環(huán)下去(注意每次退出前的循環(huán)次數(shù)可能不同):

package main

import "fmt"

func f(m map[byte]byte) string {
	bs := make([]byte, 0, 2*len(m))
	for k, v := range m {
		bs = append(bs, k, v)
	}
	return string(bs)
}

func main() {
	m := map[byte]byte{'a':'A', 'b':'B', 'c':'C'}
	s0 := f(m)
	for i := 1; ; i++{
		if s := f(m); s != s0 {
			fmt.Println(s0)
			fmt.Println(s)
			fmt.Println(i)
			return
		}
	}
}

注意:對映射進(jìn)行JSON格式化輸出中的映射條目是按照它們的鍵值排序的。 另外,從Go 1.12開始,使用fmt標(biāo)準(zhǔn)庫包中的打印函數(shù)打印映射時(shí),輸出的映射條目也是按照它們的鍵值排序的; 而在Go 1.12之前,這些打印輸出時(shí)亂序的。

在對一個(gè)映射進(jìn)行條目遍歷期間,在此映射中創(chuàng)建的新條目可能會(huì)在當(dāng)前遍歷中被遍歷出來,也可能不會(huì)。

有例為證:

package main

import "fmt"

func main() {
	m := map[int]int{0: 0, 1: 100, 2: 200}
	r, n, i:= len(m), len(m), 0
	for range m {
		m[n] = n*100
		n++
		i++
	}
	fmt.Printf("新增了%d個(gè)條目,其中%d個(gè)被遍歷出來,%d個(gè)沒有。\n",
		i, i - r, n - i,
	)
}

感謝Valentin Deleplace提出了上面兩條細(xì)節(jié)建議。

一個(gè)多返回值函數(shù)調(diào)用表達(dá)式不能和其它表達(dá)式混用在一個(gè)賦值語句的右側(cè)或者另一個(gè)函數(shù)調(diào)用的實(shí)參列表中。

關(guān)于細(xì)節(jié),請閱讀有返回值的函數(shù)的調(diào)用是一種表達(dá)式這一章節(jié)。

某些函數(shù)調(diào)用是在編譯時(shí)刻被估值的。

關(guān)于細(xì)節(jié),請閱讀哪些函數(shù)調(diào)用將在編譯時(shí)刻被估值?這一總結(jié)。

每一個(gè)方法都對應(yīng)著一個(gè)隱式聲明的函數(shù)。

關(guān)于細(xì)節(jié),請閱讀每個(gè)方法對應(yīng)著一個(gè)隱式聲明的函數(shù)這一章節(jié)。

如果兩個(gè)接口值具有相同的動(dòng)態(tài)類型并且此動(dòng)態(tài)類型不支持比較,則比較這兩個(gè)接口值將導(dǎo)致一個(gè)恐慌。

例如:

package main

func main() {
	var x interface{} = []int{}
	_ = x == x // panic
}

類型斷言可以用于將一個(gè)接口值轉(zhuǎn)換為另一個(gè)接口類型,即使此接口值的類型并未實(shí)現(xiàn)另一個(gè)接口類型。

例如:

package main

type Foo interface {
	foo()
}

type T int
func (T) foo() {}

func main() {
	var x interface{} = T(123)
	// 下面這兩行將編譯失敗。
	/*
	var _ Foo = x   // error: interface{}類型沒有實(shí)現(xiàn)Foo類型
	var _ = Foo(x)  // error: interface{}類型沒有實(shí)現(xiàn)Foo類型
	*/
	// 但是下面這行可以編譯通過。
	var _ = x.(Foo) // okay
}

一個(gè)失敗的類型斷言的可選的第二個(gè)結(jié)果是否被舍棄將影響此類型斷言的行為。

如果第二個(gè)可選結(jié)果出現(xiàn)在失敗的類型斷言中,那么此類型斷言不會(huì)導(dǎo)致恐慌。否則,恐慌將產(chǎn)生。 例如:

package main

func main() {
	var x interface{} = true
	_, _ = x.(int) // 斷言失敗,但不會(huì)導(dǎo)致恐慌。
	_ = x.(int)    // 斷言失敗,并導(dǎo)致一個(gè)恐慌。
}

關(guān)于在編譯時(shí)刻即可確定總是失敗的目標(biāo)類型為接口類型的斷言。

在編譯時(shí)刻,編譯可以發(fā)現(xiàn)某些目標(biāo)類型為接口類型的斷言是不可能成功的。比如下面這個(gè)程序中的斷言:

package main

type Ia interface {
	m()
}

type Ib interface {
	m() int
}

type T struct{}

func (T) m() {}

func main() {
	var x Ia = T{}
	_ = x.(Ib) // panic: main.T is not main.Ib
}

這樣的斷言并不會(huì)導(dǎo)致編譯失敗(但編譯后的程序?qū)⒃谶\(yùn)行時(shí)刻產(chǎn)生恐慌)。 從官方Go工具鏈1.15開始,go vet會(huì)對對這樣的斷言做出警告。

以相同實(shí)參調(diào)用兩次errors.New函數(shù)返回的兩個(gè)error值是不相等的。

原因是errors.New函數(shù)會(huì)復(fù)制輸入的字符串實(shí)參至一個(gè)局部變量并取此局部變量的指針作為返回error值的動(dòng)態(tài)值。 兩次調(diào)用會(huì)產(chǎn)生兩個(gè)不同的指針。

package main

import "fmt"
import "errors"

func main() {
	notfound := "not found"
	a, b := errors.New(notfound), errors.New(notfound)
	fmt.Println(a == b) // false
}

單向接收通道無法被關(guān)閉。

例如,下面的代碼會(huì)在編譯時(shí)候失敗。

package main

func main() {
}

func foo(c <-chan int) {
	close(c) // error: 不能關(guān)閉單向接收通道
}

發(fā)送一個(gè)值到一個(gè)已關(guān)閉的通道被視為一個(gè)非阻塞操作,該操作會(huì)導(dǎo)致恐慌。

例如,在下面的程序里,如果第二個(gè)case分支會(huì)被選中,則在運(yùn)行時(shí)刻將產(chǎn)生一個(gè)恐慌。

package main

func main() {
	var c = make(chan bool)
	close(c)
	select {
	case <-c:
	case c <- true: // panic: 向已關(guān)閉的通道發(fā)送數(shù)據(jù)
	default:
	}
}

類型可以在聲明函數(shù)體內(nèi)。

類型可以聲明在函數(shù)體內(nèi)。例如,

package main

func main() {
	type T struct{}
	type S = []int
}

對于標(biāo)準(zhǔn)編譯器,結(jié)構(gòu)體中的某些零尺寸字段的尺寸有可能會(huì)被視為一個(gè)字節(jié)。

關(guān)于細(xì)節(jié),請閱讀這個(gè)FAQ條目。

NaN != NaN,Inf == Inf。

此規(guī)則遵循IEEE-754標(biāo)準(zhǔn),并與大多數(shù)其它語言是一致的。

package main

import "fmt"
import "math"

func main() {
	var a = math.Sqrt(-1.0)
	fmt.Println(a)      // NaN
	fmt.Println(a == a) // false

	var x = 0.0
	var y = 1.0 / x
	var z = 2.0 * y
	fmt.Println(y, z, y == z) // +Inf +Inf true
}

不同代碼包中的兩個(gè)非導(dǎo)出方法名和結(jié)構(gòu)體字段名總是被視為不同的名稱。

例如,在包foo中聲明了如下的類型:

package foo

type I = interface {
	about() string
}

type S struct {
	a string
}

func (s S) about() string {
	return s.a
}

在包bar中聲明了如下的類型:

package bar

type I = interface {
	about() string
}

type S struct {
	a string
}

func (s S) about() string {
	return s.a
}

那么,

  • 兩個(gè)包中的兩個(gè)類型S的值不能相互轉(zhuǎn)換。
  • 兩個(gè)包中的兩個(gè)接口類型指定了兩個(gè)不同的方法集。
  • 類型foo.S沒有實(shí)現(xiàn)接口類型 bar.I
  • 類型bar.S沒有實(shí)現(xiàn)接口類型foo.I。
package main

import "包2/foo"
import "包2/bar"

func main() {
	var x foo.S
	var y bar.S
	var _ foo.I = x
	var _ bar.I = y

	// 下面這些行將編譯失敗。
	x = foo.S(y)
	y = bar.S(x)
	var _ foo.I = y
	var _ bar.I = x
}

在結(jié)構(gòu)體值的比較中,名為空標(biāo)識符的字段將被忽略。

比如,下面這個(gè)程序?qū)⒋蛴〕?code>true。

package main

import "fmt"

type T struct {
	_ int
	_ bool
}

func main() {
	var t1 = T{123, true}
	var t2 = T{789, false}
	fmt.Println(t1 == t2) // true
}

在某些很少見的場景中,圓括號是必需的。

例如:

package main

type T struct{x, y int}

func main() {
	// 因?yàn)閧}的煩擾,下面這三行均編譯失敗。
	/*
	if T{} == T{123, 789} {}
	if T{} == (T{123, 789}) {}
	if (T{}) == T{123, 789} {}
	var _ = func()(nil) // nil被認(rèn)為是一個(gè)類型
	*/

	// 必須加上一對小括號()才能編譯通過。
	if (T{} == T{123, 789}) {}
	if (T{}) == (T{123, 789}) {}
	var _ = (func())(nil) // nil被認(rèn)為是一個(gè)值
}

棧溢出不可被挽救,它將使程序崩潰。

在目前的主流Go編譯器實(shí)現(xiàn)中,棧溢出是致命錯(cuò)誤。一旦棧溢出發(fā)生,程序?qū)⒉豢苫謴?fù)地崩潰。

package main

func f() {
	f()
}

func main() {
	defer func() {
		recover() // 無法防止程序崩潰
	}()
	f()
}

運(yùn)行結(jié)果:

runtime: goroutine stack exceeds 1000000000-byte limit
fatal error: stack overflow

runtime stack:
...

關(guān)于更多不可恢復(fù)的致命錯(cuò)誤,請參考此篇維基文章。

某些表達(dá)式的估值順序取決于具體編譯器實(shí)現(xiàn)。

關(guān)于細(xì)節(jié),請閱讀表達(dá)式估值順序規(guī)則一文。

reflect.DeepEqual(x, y)和x == y的結(jié)果可能會(huì)不同。

如果表達(dá)式xy的類型不相同,則函數(shù)調(diào)用DeepEqual(x, y)的結(jié)果總為false,但x == y的估值結(jié)果有可能為true。

如果xy為(同類型的)兩個(gè)引用著不同其它值的指針值,則x == y的估值結(jié)果總為false,但函數(shù)調(diào)用DeepEqual(x, y)的結(jié)果可能為true,因?yàn)楹瘮?shù)reflect.DeepEqual將比較xy所引用的兩個(gè)值。

第三個(gè)區(qū)別是當(dāng)xy均處于某個(gè)循環(huán)引用鏈中時(shí),為了防止死循環(huán),DeepEqual調(diào)用的結(jié)果可能為true。

第四個(gè)區(qū)別是一個(gè)DeepEqual(x, y)調(diào)用無論如何不應(yīng)該產(chǎn)生一個(gè)恐慌,但是如果xy是兩個(gè)動(dòng)態(tài)類型相同的接口值并且它們的動(dòng)態(tài)類型是不可比較類型的時(shí)候,x == y將產(chǎn)生一個(gè)恐慌。

一個(gè)展示了這些不同的例子:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	type Book struct {page int}
	x := struct {page int}{123}
	y := Book{123}
	fmt.Println(reflect.DeepEqual(x, y)) // false
	fmt.Println(x == y)                  // true

	z := Book{123}
	fmt.Println(reflect.DeepEqual(&z, &y)) // true
	fmt.Println(&z == &y)                  // false

	type Node struct{peer *Node}
	var q, r, s Node
	q.peer = &q // 形成一個(gè)循環(huán)引用鏈
	r.peer = &s // 形成一個(gè)循環(huán)引用鏈
	s.peer = &r
	println(reflect.DeepEqual(&q, &r)) // true
	fmt.Println(q == r)                // false

	var f1, f2 func() = nil, func(){}
	fmt.Println(reflect.DeepEqual(f1, f1)) // true
	fmt.Println(reflect.DeepEqual(f2, f2)) // false

	var a, b interface{} = []int{1, 2}, []int{1, 2}
	fmt.Println(reflect.DeepEqual(a, b)) // true
	fmt.Println(a == b)                  // 產(chǎn)生恐慌
}

注意:如果傳遞給一個(gè)DeepEqual調(diào)用的兩個(gè)實(shí)參均為函數(shù)類型值,則此調(diào)用只有在這兩個(gè)實(shí)參都為nil并且它們的類型相同的情況下才返回true。 比較元素中含有函數(shù)值的容器值或者比較字段中含有函數(shù)值的結(jié)構(gòu)體值也是類似的。 另外要注意:如果兩個(gè)同類型切片共享相同的元素序列(即它們的長度相同并且它們的各對相應(yīng)元素的地址也相同),則使用DeepEqual比較它們時(shí)返回的結(jié)果總是為true,即使它們的元素中含有函數(shù)值。 一個(gè)例子:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	a := [1]func(){func(){}}
	b := a
	fmt.Println(reflect.DeepEqual(a, a))       // false
	fmt.Println(reflect.DeepEqual(a[:], a[:])) // true
	fmt.Println(reflect.DeepEqual(a[:], b[:])) // false
	a[0], b[0] = nil, nil
	fmt.Println(reflect.DeepEqual(a[:], b[:])) // true
}

reflect.Value.Bytes()方法返回一個(gè)[]byte值,它的元素類型byte可能并非屬主參數(shù)代表的Go切片值的元素類型。

假設(shè)一個(gè)自定義類型MyByte的底層類型為內(nèi)置類型byte,我們知道Go類型系統(tǒng)禁止切片類型[]MyByte的值轉(zhuǎn)換為類型[]byte。 但是,當(dāng)前的reflect.Value類型的Bytes方法的實(shí)現(xiàn)可以幫我們繞過這個(gè)限制。 此實(shí)現(xiàn)應(yīng)該是違反了Go類型系統(tǒng)的規(guī)則。

例子:

package main

import "bytes"
import "fmt"
import "reflect"

type MyByte byte

func main() {
	var mybs = []MyByte{'a', 'b', 'c'}
	var bs []byte

	// bs = []byte(mybs) // this line fails to compile

	v := reflect.ValueOf(mybs)
	bs = v.Bytes() // okay. Violating Go type system.
	fmt.Println(bytes.HasPrefix(bs, []byte{'a', 'b'})) // true

	bs[1], bs[2] = 'r', 't'
	fmt.Printf("%s \n", mybs) // art
}

雖然這違反了Go類型系統(tǒng)的規(guī)則,但是貌似此違反并沒有什么害處,相反,它帶來了一些好處。 比如,我們可以將bytes標(biāo)準(zhǔn)庫包中提供的函數(shù)(間接)應(yīng)用到[]MyByte值上,如上例所示。

注意:reflect.Value.Bytes()方法以后可能會(huì)被移除。

我們應(yīng)該使用os.IsNotExist(err)而不是err == os.ErrNotExist來檢查文件是否存在。

使用err == os.ErrNotExist可能漏掉一些錯(cuò)誤。

package main

import (
	"fmt"
	"os"
)

func main() {
	_, err := os.Stat("a-nonexistent-file.abcxyz")
	fmt.Println(os.IsNotExist(err))    // true
	fmt.Println(err == os.ErrNotExist) // false
}

如果你的項(xiàng)目只支持Go 1.13+,則更推薦使用errors.Is(err, os.ErrNotExist)來檢查文件是否存在。

package main

import (
	"errors"
	"fmt"
	"os"
)

func main() {
	_, err := os.Stat("a-nonexistent-file.abcxyz")
	fmt.Println(errors.Is(err, os.ErrNotExist)) // true
}

flag標(biāo)準(zhǔn)庫包對待布爾命令選項(xiàng)不同于數(shù)值和字符串選項(xiàng)。

傳遞程序選項(xiàng)有三種形式。

  1. -flag:僅適用于布爾選項(xiàng)。
  2. -flag=x:用于任何類型的選項(xiàng)。.
  3. -flag x:僅用于非布爾選項(xiàng)。

請注意,使用第一種形式的布爾選項(xiàng)將被視為最后一個(gè)選項(xiàng),其后面的所有項(xiàng)都被視為參數(shù)。

package main

import "fmt"
import "flag"

var b = flag.Bool("b", true, "一個(gè)布爾選項(xiàng)")
var i = flag.Int("i", 123, "一個(gè)整數(shù)選項(xiàng)")
var s = flag.String("s", "hi", "一個(gè)字符串選項(xiàng)")

func main() {
	flag.Parse()
	fmt.Print("b=", *b, ", i=", *i, ", s=", *s, "\n")
	fmt.Println("arguments:", flag.Args())
}

如果我們用下面顯示的標(biāo)志和參數(shù)運(yùn)行此程序

./exampleProgram -b false -i 789 -s bye arg0 arg1

輸出結(jié)果會(huì)是:

b=true, i=123, s=hi
arguments: [false -i 789 -s bye arg0 arg1]

這個(gè)輸出顯然不是我們所期望的。

我們應(yīng)該像這樣傳遞選項(xiàng)和參數(shù):

./exampleProgram -b=false -i 789 -s bye arg0 arg1

或者

./exampleProgram -i 789 -s bye -b arg0 arg1

以獲取我們期望的輸出:

b=true, i=789, s=bye
arguments: [arg0 arg1]

[Sp|Fp|P]rintf函數(shù)支持位置參數(shù)。

下面的程序會(huì)打印coco。

package main

import "fmt"

func main() {
	// The next line prints: coco
	fmt.Printf("%[2]v%[1]v%[2]v%[1]v", "o", "c")
}


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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號