Go語言 Go細節(jié)

2023-02-16 17:41 更新

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

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

示例:

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流程控制代碼塊中的數字常量case表達式不能重復,但是布爾常量case表達式可以重復。

例如,下面的代碼在編譯時會失敗。

package main

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

但是下面的代碼在編譯時是沒問題的。

package main

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

關于原因,請閱讀這個issue。 此行為依賴于編譯器。事實上,標準編譯器同樣不允許重復的字符串case表達式,但是gccgo編譯器卻允許。

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

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

package main

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

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

例如,下列程序會打印出true。

package main

import "fmt"

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

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

例如:

package main

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

下面程序的結果會打印什么?true還是false? 答案是true。 關于原因請閱讀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分支代碼塊必須是顯式的。

例如,下面的程序會在編譯時將失敗。

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

		End: // syntax error: 標簽后缺少語句
	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
}

另外,我們可以在標簽End:之后加一個分號,如下所示:

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

		End:;
	default:
		r = 1
	}
	return
}

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

嵌套的延遲函數調用可以修改外層函數的返回結果。

例如:

package main

import "fmt"

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

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

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

某些recover函數調用是空操作。

我們需要在正確的地方調用recover函數。 關于細節(jié),請閱讀 在正確的位置調用內置函數recover一文。

我們可以使用os.Exit函數調用退出一個程序和使用runtime.Goexit函數調用退出一個協程。

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

示例:

// exit-example.go
package main

import "os"
import "time"

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

運行:

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

我們可以通過調用runtime.Goexit函數退出一個goroutine。 runtime.Goexit函數沒有參數。

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

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ōu)先級低于解引用運算符*和取地址運算符&,解引用運算符和取地址運算符的優(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
}

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

package main

func main() {
}

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

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

關于原因請閱讀運算操作符一文。

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

例如:

package main

type MyInt int64
type Ta    *int64
type Tb    *MyInt

func main() {
	var a Ta
	var b Tb

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

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

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

兩個零尺寸值的地址是否相等時依賴于具體編譯器實現以及具體編譯器版本。

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)

	// 對于標準編譯器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
}

上面代碼中所示的輸出是針對標準編譯器1.19版本的。

一個指針類型的基類型可以為此指針類型自身。

例如:

package main

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

類似的,

  • 一個切片類型的元素類型可以是此切片類型自身,
  • 一個映射類型的元素類型可以是此映射類型自身,
  • 一個通道類型的元素類型可以是此通道類型自身,
  • 一個函數類型的輸入參數和返回結果值類型可以是此函數類型自身。
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))))
}

有關選擇器縮寫形式的細節(jié)。

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

我們總是不能使用二級以上指針來選擇結構體字段和方法。

package main

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

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

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()  // 沒問題,因為類型*T也有一個m方法。
	p.m()   // error: 類型P沒有名為m的字段或者方法。
	pp.m()  // error: 類型PP沒有名為m的字段或者方法。
	tpp.m() // error: 類型**T沒有名為m的字段或者方法。
}

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

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

在某些情形下,我們可以將數組指針當作數組來用。

關于細節(jié),請閱讀把數組指針當做數組來使用這一章節(jié)。

從nil映射中讀取元素不會導致崩潰,讀取結果是一個零元素值。

例如,函數Foo1Foo2是等價的,但是函數Foo2比函數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"]
}

從一個nil映射中刪除一個條目不會導致崩潰,這是一個空操作。

例如,下面這個程序不會因為恐慌而崩潰。

package main

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

append函數調用的結果可能會與原始切片共享一些元素,也可能不共享任何元素。

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

從一個基礎切片派生出的子切片的長度可能大于基礎切片的長度。

例如:

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
}

關于細節(jié),請閱讀從數組或者切片派生切片這一章節(jié)。

從一個nil切片中派生子切片是允許的,只要子切片表達式中使用的所有索引都為零,則不會有恐慌產生,結果子切片同樣是一個nil切片。

例如,下面的程序在運行時刻不會產生恐慌。

package main

import "fmt"

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

關于細節(jié),請閱讀從數組或者切片派生切片這一章節(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數組指針時,如果忽略或省略第二個迭代變量,則此遍歷是沒問題的。遍歷中的循環(huán)步數為相應數組類型的長度。

例如,下面的程序會輸出01234

package main

import "fmt"

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

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

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

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

例如,下面的程序將編譯失敗。

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

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

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

例如,下面的程序將編譯失敗。

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

這個特性可以用于在編譯時刻斷言某些條件。

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

原因是一個數組值的元素和此數組存儲在同一個內存塊中。 但是切片的情況大不相同

一個例子:

package main

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

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

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

	/* 修改元素值。 */

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

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

原因和上一個細節(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]

	// 下面幾行將編譯失敗,因為不可從不可尋址的數組派生切片。
	/*
	_ = [...]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做為鍵值的條目放如映射就宛如把條目放入黑洞一樣。

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

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之前,兩個fmt.Println(m)調用均打印出map[NaN:<nil> NaN:<nil>]。

字符串轉換為byte切片或rune切片后的結果切片的容量可能會大于長度。

我們不應該假設結果切片的長度和容量總是相等的。

在下面的例子中,如果最后一個fmt.Println行被刪除,在其前面的兩行會打印相同的值32;否則,一個打印32,一個打印8(對于標準編譯器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)
}

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

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

對于這兩個循環(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
}

一個映射中的條目的遍歷次序在兩次遍歷中可能并不相同。我們可以認為映射中的條目的遍歷次序是隨機的。

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

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

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

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

有例為證:

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個條目,其中%d個被遍歷出來,%d個沒有。\n",
		i, i - r, n - i,
	)
}

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

一個多返回值函數調用表達式不能和其它表達式混用在一個賦值語句的右側或者另一個函數調用的實參列表中。

關于細節(jié),請閱讀有返回值的函數的調用是一種表達式這一章節(jié)。

某些函數調用是在編譯時刻被估值的。

關于細節(jié),請閱讀哪些函數調用將在編譯時刻被估值?這一總結。

每一個方法都對應著一個隱式聲明的函數。

關于細節(jié),請閱讀每個方法對應著一個隱式聲明的函數這一章節(jié)。

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

例如:

package main

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

類型斷言可以用于將一個接口值轉換為另一個接口類型,即使此接口值的類型并未實現另一個接口類型。

例如:

package main

type Foo interface {
	foo()
}

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

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

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

如果第二個可選結果出現在失敗的類型斷言中,那么此類型斷言不會導致恐慌。否則,恐慌將產生。 例如:

package main

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

關于在編譯時刻即可確定總是失敗的目標類型為接口類型的斷言。

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

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
}

這樣的斷言并不會導致編譯失?。ǖ幾g后的程序將在運行時刻產生恐慌)。 從官方Go工具鏈1.15開始,go vet會對對這樣的斷言做出警告。

以相同實參調用兩次errors.New函數返回的兩個error值是不相等的。

原因是errors.New函數會復制輸入的字符串實參至一個局部變量并取此局部變量的指針作為返回error值的動態(tài)值。 兩次調用會產生兩個不同的指針。

package main

import "fmt"
import "errors"

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

單向接收通道無法被關閉。

例如,下面的代碼會在編譯時候失敗。

package main

func main() {
}

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

發(fā)送一個值到一個已關閉的通道被視為一個非阻塞操作,該操作會導致恐慌。

例如,在下面的程序里,如果第二個case分支會被選中,則在運行時刻將產生一個恐慌。

package main

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

類型可以在聲明函數體內。

類型可以聲明在函數體內。例如,

package main

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

對于標準編譯器,結構體中的某些零尺寸字段的尺寸有可能會被視為一個字節(jié)。

關于細節(jié),請閱讀這個FAQ條目

NaN != NaN,Inf == Inf。

此規(guī)則遵循IEEE-754標準,并與大多數其它語言是一致的。

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
}

不同代碼包中的兩個非導出方法名和結構體字段名總是被視為不同的名稱。

例如,在包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
}

那么,

  • 兩個包中的兩個類型S的值不能相互轉換。
  • 兩個包中的兩個接口類型指定了兩個不同的方法集。
  • 類型foo.S沒有實現接口類型 bar.I。
  • 類型bar.S沒有實現接口類型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
}

在結構體值的比較中,名為空標識符的字段將被忽略。

比如,下面這個程序將打印出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() {
	// 因為{}的煩擾,下面這三行均編譯失敗。
	/*
	if T{} == T{123, 789} {}
	if T{} == (T{123, 789}) {}
	if (T{}) == T{123, 789} {}
	var _ = func()(nil) // nil被認為是一個類型
	*/

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

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

在目前的主流Go編譯器實現中,棧溢出是致命錯誤。一旦棧溢出發(fā)生,程序將不可恢復地崩潰。

package main

func f() {
	f()
}

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

運行結果:

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

runtime stack:
...

關于更多不可恢復的致命錯誤,請參考此篇維基文章。

某些表達式的估值順序取決于具體編譯器實現。

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

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

如果表達式xy的類型不相同,則函數調用DeepEqual(x, y)的結果總為false,但x == y的估值結果有可能為true

如果xy為(同類型的)兩個引用著不同其它值的指針值,則x == y的估值結果總為false,但函數調用DeepEqual(x, y)的結果可能為true,因為函數reflect.DeepEqual將比較xy所引用的兩個值。

第三個區(qū)別是當xy均處于某個循環(huán)引用鏈中時,為了防止死循環(huán),DeepEqual調用的結果可能為true

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

一個展示了這些不同的例子:

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 // 形成一個循環(huán)引用鏈
	r.peer = &s // 形成一個循環(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)                  // 產生恐慌
}

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

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()方法返回一個[]byte值,它的元素類型byte可能并非屬主參數代表的Go切片值的元素類型。

假設一個自定義類型MyByte的底層類型為內置類型byte,我們知道Go類型系統(tǒng)禁止切片類型[]MyByte的值轉換為類型[]byte。 但是,當前的reflect.Value類型的Bytes方法的實現可以幫我們繞過這個限制。 此實現應該是違反了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標準庫包中提供的函數(間接)應用到[]MyByte值上,如上例所示。

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

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

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

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
}

如果你的項目只支持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標準庫包對待布爾命令選項不同于數值和字符串選項。

傳遞程序選項有三種形式。

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

請注意,使用第一種形式的布爾選項將被視為最后一個選項,其后面的所有項都被視為參數。

package main

import "fmt"
import "flag"

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

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

如果我們用下面顯示的標志和參數運行此程序

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

輸出結果會是:

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

這個輸出顯然不是我們所期望的。

我們應該像這樣傳遞選項和參數:

./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函數支持位置參數。

下面的程序會打印coco。

package main

import "fmt"

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


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號