Go語言 Go技巧

2023-02-16 17:42 更新

如何強(qiáng)制一個(gè)代碼包的使用者總是使用帶字段名稱的組合字面量來表示此代碼包中的結(jié)構(gòu)體類型的值?

代碼包的開發(fā)者可以在一個(gè)結(jié)構(gòu)體類型定義里放置一個(gè)非導(dǎo)出的零尺寸的字段,這樣編譯器將會(huì)禁止代碼包的使用者使用含有一些字段但卻不含有字段名字的組合字面量來創(chuàng)建此結(jié)構(gòu)體類型的值。

例如:

// foo.go
package foo

type Config struct {
	_    [0]int
	Name string
	Size int
}
// main.go
package main

import "foo"

func main() {
	//_ = foo.Config{[0]int{}, "bar", 123} // 編譯不通過
	_ = foo.Config{Name: "bar", Size: 123} // 編譯沒問題
}

請(qǐng)盡量不要把零尺寸的非導(dǎo)出字段用做結(jié)構(gòu)體的最后一個(gè)字段,因?yàn)?a href="http://m.o2fo.com/dypav/dypav-8vrj3qtk.html" target="_blank">這樣做會(huì)有可能會(huì)增大結(jié)構(gòu)體類型的尺寸而導(dǎo)致一些內(nèi)存浪費(fèi)。

如何使一個(gè)結(jié)構(gòu)體類型不可比較?

有時(shí)候,我們想要避免一個(gè)自定義的結(jié)構(gòu)體類型被用做一個(gè)映射的鍵值類型,那么我們可以放置一個(gè)非導(dǎo)出的零尺寸的不可比較類型的字段在結(jié)構(gòu)體類型中以使此結(jié)構(gòu)體類型不可比較。 例如:

package main

type T struct {
	dummy        [0]func()
	AnotherField int
}

var x map[T]int // 編譯錯(cuò)誤:非法的鍵值類型

func main() {
	var a, b T
	_ = a == b // 編譯錯(cuò)誤:非法的比較
}

不要使用其中涉及到的表達(dá)式之間會(huì)相互干涉的賦值語句。

目前(Go 1.19),在一些多值賦值中有一些表達(dá)式估值順序是未指定的。 因此,如果一個(gè)多值賦值語句中涉及的表達(dá)式會(huì)相互干涉,或者不太容易確定是否會(huì)相互干涉,我們應(yīng)該將此多值賦值語句分拆成多個(gè)單值賦值語句。

事實(shí)上,在一些寫得很糟糕的代碼中,單值賦值中的表達(dá)式求值順序也有可能是有歧義的。 例如,下面的程序可能會(huì)打印[7 0 9]、[0 8 9]或者[7 8 9],依賴于具體編譯器實(shí)現(xiàn)。

package main

import "fmt"

var a = &[]int{1, 2, 3}
var i int
func f() int {
	i = 1
	a = &[]int{7, 8, 9}
	return 0
}

func main() {
	// 表達(dá)式"a"、"i"和"f()"的估值順序未定義。
	(*a)[i] = f()
	fmt.Println(*a)
}

如何模擬一些其它語言中支持的for i in 0..N循環(huán)代碼塊?

我們可以通過遍歷一個(gè)元素尺寸為零的數(shù)組或者一個(gè)空數(shù)組指針來模擬這樣的循環(huán)。 例如:

package main

import "fmt"

func main() {
	const N = 5

	for i := range [N]struct{}{} {
		fmt.Println(i)
	}
	for i := range [N][0]int{} {
		fmt.Println(i)
	}
	for i := range (*[N]int)(nil) {
		fmt.Println(i)
	}
}

當(dāng)我們廢棄一個(gè)仍在使用的切片中的一些元素時(shí),我們應(yīng)該重置這些元素中的指針來避免暫時(shí)性的內(nèi)存泄漏。

關(guān)于細(xì)節(jié),請(qǐng)閱讀如何刪除切片元素因?yàn)槲粗刂脕G失的切片元素中的指針而造成的臨時(shí)性內(nèi)存泄露。

一些標(biāo)準(zhǔn)包中的某些類型的值不期望被復(fù)制。

bytes.Buffer類型、strings.Builder類型以及在sync標(biāo)準(zhǔn)庫包里的類型的值不推薦被復(fù)制。 (它們確實(shí)不應(yīng)該被復(fù)制,盡管在某些特定情形下復(fù)制它們或許是沒有問題的。)

strings.Builder的實(shí)現(xiàn)會(huì)在運(yùn)行時(shí)刻探測到非法的strings.Builder值復(fù)制。 一旦這樣的復(fù)制被發(fā)現(xiàn),就會(huì)產(chǎn)生恐慌。例如:

package main

import "strings"

func main() {
	var b strings.Builder
	b.WriteString("hello ")
	var b2 = b
	b2.WriteString("world!") // 一個(gè)恐慌將在這里產(chǎn)生
}

復(fù)制標(biāo)準(zhǔn)庫包sync中類型的值會(huì)被Go官方工具鏈提供的go vet命令檢測到并被警告。

// demo.go
package demo

import "sync"

func f(m sync.Mutex) { // warning: f passes lock by value: sync.Mutex
	m.Lock()
	defer m.Unlock()
	// do something ...
}
$ go vet demo.go
./demo.go:5: f passes lock by value: sync.Mutex

復(fù)制bytes.Buffer的值不會(huì)在運(yùn)行時(shí)被檢查到,也不會(huì)被go vet命令所檢測到。 千萬要小心不要隨意這樣做。

我們可以利用memclr優(yōu)化來重置數(shù)組或者切片中一段連續(xù)的元素。

關(guān)于細(xì)節(jié),請(qǐng)閱讀memclr優(yōu)化

如何在不導(dǎo)入reflect標(biāo)準(zhǔn)庫包的情況下檢查一個(gè)值是否擁有某個(gè)方法。

可以使用下面的例子中的方法。 (假設(shè)需要被檢查的方法的描述是M(int) string。)

package main

import "fmt"

type A int
type B int
func (b B) M(x int) string {
	return fmt.Sprint(b, ": ", x)
}

func check(v interface{}) bool {
	_, has := v.(interface{M(int) string})
	return has
}

func main() {
	var a A = 123
	var b B = 789
	fmt.Println(check(a)) // false
	fmt.Println(check(b)) // true
}

如何高效且完美地克隆一個(gè)切片?

關(guān)于細(xì)節(jié)請(qǐng)閱讀這篇wiki文章這篇wiki文章

在部分場景下我們應(yīng)該使用三下標(biāo)子切片形式。

假設(shè)一個(gè)包提供了一個(gè)func NewX(...Option) *X函數(shù),并且這個(gè)函數(shù)的實(shí)現(xiàn)將輸入選項(xiàng)與一些內(nèi)部默認(rèn)選項(xiàng)合并,那么下面的實(shí)現(xiàn)是不推薦的。

func NewX(opts ...Option) *X {
	options := append(opts, defaultOpts...)
	// 使用合并后選項(xiàng)來創(chuàng)建一個(gè)X值并返回其指針。
	// ...
}

上述實(shí)現(xiàn)不被推薦的原因是append函數(shù)調(diào)用可能會(huì)修改輸入實(shí)參opts的底層潛在Option元素序列。 對(duì)大多數(shù)場景,這可能是沒問題的。但是對(duì)某些特殊場景,這有可能會(huì)導(dǎo)致后續(xù)代碼執(zhí)行產(chǎn)生不期望的結(jié)果。

為了避免輸入實(shí)參的底層Option元素序列被修改,我們應(yīng)該使用下面的實(shí)現(xiàn)方法:

func NewX(opts ...Option) *X {
	// 改用三下標(biāo)子切片格式。
	opts = append(opts[:len(opts):len(opts)], defaultOpts...)
	// 使用合并后選項(xiàng)來創(chuàng)建一個(gè)X值并返回其指針。
	// ...
}

另一方面,對(duì)于NewX函數(shù)的調(diào)用者來說,不應(yīng)該依賴于此函數(shù)的具體實(shí)現(xiàn),所以最好使用三下標(biāo)子切片形式options[:len(options):cap(options)]來傳遞實(shí)參。

另外一個(gè)需要使用三下標(biāo)子切片格式的場景在這篇wiki文章中被提及。

三下標(biāo)子切片格式的一個(gè)缺點(diǎn)是它們有些冗長。 事實(shí)上,我曾經(jīng)提了一個(gè)建議來讓三下標(biāo)格式看上起簡潔得多。 但是此建議被否決了。

使用匿名函數(shù)來使部分延遲函數(shù)調(diào)用盡早執(zhí)行。

關(guān)于細(xì)節(jié),請(qǐng)閱讀這篇文章。

確保并表明一個(gè)自定義類型實(shí)現(xiàn)了指定的接口類型。

我們可以將一個(gè)自定義類型的一個(gè)值賦給指定接口類型的一個(gè)變量來確保此自定義類型實(shí)現(xiàn)了指定接口類型。 更重要的是,這樣可以表明此自定義類型實(shí)現(xiàn)了指定接口類型。 使用自解釋的代碼編寫文檔比使用注釋來編寫文檔要自然得多。

package myreader

import "io"

type MyReader uint16

func NewMyReader() *MyReader {
	var mr MyReader
	return &mr
}

func (mr *MyReader) Read(data []byte) (int, error) {
	switch len(data) {
	default:
		*mr = MyReader(data[0]) << 8 | MyReader(data[1])
		return 2, nil
	case 2:
		*mr = MyReader(data[0]) << 8 | MyReader(data[1])
	case 1:
		*mr = MyReader(data[0])
	case 0:
	}
	return len(data), io.EOF
}

// 下面三行中的任一行都可以保證類型*MyReader實(shí)現(xiàn)
// 了接口io.Reader。
var _ io.Reader = NewMyReader()
var _ io.Reader = (*MyReader)(nil)
func _() {_ = io.Reader(nil).(*MyReader)}

一些編譯時(shí)刻斷言技巧。

除了上一個(gè)技巧中提到過的編譯時(shí)刻斷言技巧,下面將要介紹更多編譯時(shí)刻斷言技巧。

下面是一些方法用來在編譯時(shí)刻保證常量N不小于另一個(gè)常量M

// 下面任一行均可保證N >= M
func _(x []int) {_ = x[N-M]}
func _(){_ = []int{N-M: 0}}
func _([N-M]int){}
var _ [N-M]int
const _ uint = N-M
type _ [N-M]int

// 如果M和N都是正整數(shù)常量,則我們也可以使用下一行所示的方法。
var _ uint = N/M - 1

另一個(gè)方法是借鑒@lukechampine的一個(gè)點(diǎn)子。 此點(diǎn)子利用了容器組合字面量中不能出現(xiàn)重復(fù)的常量鍵值這一規(guī)則。

var _ = map[bool]struct{}{false: struct{}{}, N>=M: struct{}{}}

此方法看上去有些冗長,但是它更加通用。它可以用來斷言任何條件。 其實(shí),它也可以不必很冗長,但需要多消耗一點(diǎn)(完全可以忽略的)內(nèi)存,如下面所示:

var _ = map[bool]int{false: 0, N>=M: 1}

類似地,下面是斷言兩個(gè)整數(shù)常量相等的方法:

var _ [N-M]int; var _ [M-N]int
type _ [N-M]int; type _ [M-N]int
const _, _ uint = N-M, M-N
func _([N-M]int, [M-N]int) {}

var _ = map[bool]int{false: 0, M==N: 1}

var _ = [1]int{M-N: 0} // 唯一被允許的元素索引下標(biāo)為0
var _ = [1]int{}[M-N]  // 唯一被允許的元素索引下標(biāo)為0

var _ [N-M]int = [M-N]int{}

最后一行的靈感同樣來自于Luke Champine的一條tweet。

下面是一些用來斷言一個(gè)常量字符串是不是一個(gè)空串的方法。

type _ [len(aStringConstant)-1]int
var _ = map[bool]int{false: 0, aStringConstant != "": 1}
var _ = aStringConstant[:1]
var _ = aStringConstant[0]
const _ = 1/len(aStringConstant)

最后一行借鑒自Jan Mercl的一個(gè)點(diǎn)子。

有時(shí)候,為了避免包級(jí)變量消耗太多的內(nèi)存,我們可以把斷言代碼放在一個(gè)名為空標(biāo)識(shí)符的函數(shù)體中。 例如:

func _() {
	var _ = map[bool]int{false: 0, N>=M: 1}
	var _ [N-M]int
}

如何聲明一個(gè)最大的int和uint常量?

const MaxUint = ^uint(0)
const MaxInt = int(^uint(0) >> 1)

如何在編譯時(shí)刻決定系統(tǒng)原生字的尺寸?

這個(gè)技巧和Go無關(guān)。

const Is64bitArch = ^uint(0) >> 63 == 1
const Is32bitArch = ^uint(0) >> 63 == 0
const WordBits = 32 << (^uint(0) >> 63) // 64或32

如何保證64位原子函數(shù)調(diào)用中操作的64位整數(shù)的地址在32位架構(gòu)上總是64位對(duì)齊的?

關(guān)于細(xì)節(jié),請(qǐng)閱讀關(guān)于Go值的內(nèi)存布局一文。

盡量避免將大尺寸的值包裹在接口值中。

當(dāng)一個(gè)非接口值被賦值給一個(gè)接口值時(shí),此非接口值的一個(gè)副本將被包裹到此接口值中。 副本復(fù)制的開銷和非接口值的尺寸成正比。尺寸越大,復(fù)制開銷越大。 所以請(qǐng)盡量避免將大尺寸的值包裹到接口值中。

在下面的例子中,后兩個(gè)打印調(diào)用的成本要比前兩個(gè)低得多。

package main

import "fmt"

func main() {
	var a [1000]int

	// 這兩行的開銷相對(duì)較大,因?yàn)閿?shù)組a中的元素都將被復(fù)制。
	fmt.Println(a)
	fmt.Printf("Type of a: %T\n", a)

	// 這兩行的開銷較小,數(shù)組a中的元素沒有被復(fù)制。
	fmt.Printf("%v\n", a[:])
	fmt.Println("Type of a:", fmt.Sprintf("%T", &a)[1:])
}

關(guān)于不同種類的類型的尺寸,請(qǐng)閱讀值復(fù)制成本一文。

利用BCE(邊界檢查消除)進(jìn)行性能優(yōu)化。

請(qǐng)閱讀此文來獲知什么是邊界檢查消除(BCE)以及目前的標(biāo)準(zhǔn)編譯器對(duì)BCE的支持程度。

下面是一個(gè)利用了BCE進(jìn)行性能優(yōu)化的例子:

package main

import (
	"strings"
	"testing"
)

func NumSameBytes_1(x, y string) int {
	if len(x) > len(y) {
		x, y = y, x
	}
	for i := 0; i < len(x); i++ {
		if x[i] != y[i] {
			return i
		}
	}
	return len(x)
}

func NumSameBytes_2(x, y string) int {
	if len(x) > len(y) {
		x, y = y, x
	}
	if len(x) <= len(y) { // 雖然代碼多了,但是效率提高了
		for i := 0; i < len(x); i++ {
			if x[i] != y[i] { // 邊界檢查被消除了
				return i
			}
		}
	}
	return len(x)
}

var x = strings.Repeat("hello", 100) + " world!"
var y = strings.Repeat("hello", 99) + " world!"

func BenchmarkNumSameBytes_1(b *testing.B) {
	for i := 0; i < b.N; i++ {
		_ = NumSameBytes_1(x, y)
	}
}

func BenchmarkNumSameBytes_2(b *testing.B) {
	for i := 0; i < b.N; i++ {
		_ = NumSameBytes_2(x, y)
	}
}

從下面所示的基準(zhǔn)測試結(jié)果來看,函數(shù)NumSameBytes_2比函數(shù)NumSameBytes_1效率更高。

BenchmarkNumSameBytes_1-4   	10000000	       669 ns/op
BenchmarkNumSameBytes_2-4   	20000000	       450 ns/op

請(qǐng)注意:標(biāo)準(zhǔn)編譯器(gc)的每個(gè)新的主版本都會(huì)有很多小的改進(jìn)。 上例中所示的優(yōu)化從gc 1.11開始才有效。 未來的gc版本可能會(huì)變得更加智能,以使函數(shù)NumSameBytes_2中使用技巧變得不再必要。 事實(shí)上,從gc 1.11開始,如果xy是兩個(gè)切片,即使上例中使用小技巧沒有被使用,y[i]中的邊界檢查也已經(jīng)被消除了。


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

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)