for i in 0..N
循环代码块?
memclr
优化来重置数组或者切片中一段连续的元素。
reflect
标准库包的情况下检查一个值是否拥有某个方法。
代码包的开发者可以在一个结构体类型定义里放置一个非导出的零尺寸的字段,这样编译器将会禁止代码包的使用者使用含有一些字段但却不含有字段名字的组合字面量来创建此结构体类型的值。
例如:// 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} // 编译没问题
}
请尽量不要把零尺寸的非导出字段用做结构体的最后一个字段,因为这样做会有可能会增大结构体类型的尺寸而导致一些内存浪费。
package main
type T struct {
dummy [0]func()
AnotherField int
}
var x map[T]int // 编译错误:非法的键值类型
func main() {
var a, b T
_ = a == b // 编译错误:非法的比较
}
目前(Go 1.23),在一些多值赋值中有一些表达式估值顺序是未指定的。 因此,如果一个多值赋值语句中涉及的表达式会相互干涉,或者不太容易确定是否会相互干涉,我们应该将此多值赋值语句分拆成多个单值赋值语句。
事实上,在一些写得很糟糕的代码中,单值赋值中的表达式求值顺序也有可能是有歧义的。 例如,下面的程序可能会打印[7 0 9]
、[0 8 9]
或者[7 8 9]
,依赖于具体编译器实现。
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() {
// 表达式"a"、"i"和"f()"的估值顺序未定义。
(*a)[i] = f()
fmt.Println(*a)
}
换言之,一条赋值语句中的某个函数调用表达式的估值有可能会影响到其它非函数调用表达式的估值结果。 请阅读表达式估值顺序规则以获取更多细节。
for i in 0..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)
}
}
关于细节,请阅读如何删除切片元素和因为未重置丢失的切片元素中的指针而造成的临时性内存泄露。
bytes.Buffer
类型、strings.Builder
类型以及在sync
标准库包里的类型的值不推荐被复制。
(它们确实不应该被复制,尽管在某些特定情形下复制它们或许是没有问题的。)
strings.Builder
的实现会在运行时刻探测到非法的strings.Builder
值复制。
一旦这样的复制被发现,就会产生恐慌。例如:
package main
import "strings"
func main() {
var b strings.Builder
b.WriteString("hello ")
var b2 = b
b2.WriteString("world!") // 一个恐慌将在这里产生
}
复制标准库包
sync
中类型的值会被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
复制bytes.Buffer
的值不会在运行时被检查到,也不会被go vet
命令所检测到。
千万要小心不要随意这样做。
memclr
优化来重置数组或者切片中一段连续的元素。
关于细节,请阅读memclr
优化。
reflect
标准库包的情况下检查一个值是否拥有某个方法。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
}
func NewX(...Option) *X
函数,并且这个函数的实现将输入选项与一些内部默认选项合并,那么下面的实现是不推荐的。
func NewX(opts ...Option) *X {
options := append(opts, defaultOpts...)
// 使用合并后选项来创建一个X值并返回其指针。
// ...
}
上述实现不被推荐的原因是append
函数调用可能会修改输入实参opts
的底层潜在Option
元素序列。
对大多数场景,这可能是没问题的。但是对某些特殊场景,这有可能会导致后续代码执行产生不期望的结果。
Option
元素序列被修改,我们应该使用下面的实现方法:
func NewX(opts ...Option) *X {
// 改用三下标子切片格式。
opts = append(opts[:len(opts):len(opts)], defaultOpts...)
// 使用合并后选项来创建一个X值并返回其指针。
// ...
}
另一方面,对于NewX
函数的调用者来说,不应该依赖于此函数的具体实现,所以最好使用三下标子切片形式options[:len(options):cap(options)]
来传递实参。
另外一个需要使用三下标子切片格式的场景在这篇wiki文章中被提及。
三下标子切片格式的一个缺点是它们有些冗长。 事实上,我曾经提了一个建议来让三下标格式看上起简洁得多。 但是此建议被否决了。
关于细节,请阅读这篇文章。
我们可以将一个自定义类型的一个值赋给指定接口类型的一个变量来确保此自定义类型实现了指定接口类型。 更重要的是,这样可以表明此自定义类型实现了指定接口类型。 使用自解释的代码编写文档比使用注释来编写文档要自然得多。
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实现
// 了接口io.Reader。
var _ io.Reader = NewMyReader()
var _ io.Reader = (*MyReader)(nil)
func _() {_ = io.Reader(nil).(*MyReader)}
除了上一个技巧中提到过的编译时刻断言技巧,下面将要介绍更多编译时刻断言技巧。
下面是一些方法用来在编译时刻保证常量N
不小于另一个常量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都是正整数常量,则我们也可以使用下一行所示的方法。
var _ uint = N/M - 1
另一个方法是借鉴@lukechampine的一个点子。
此点子利用了容器组合字面量中不能出现重复的常量键值这一规则。
var _ = map[bool]struct{}{false: struct{}{}, N>=M: struct{}{}}
此方法看上去有些冗长,但是它更加通用。它可以用来断言任何条件。
其实,它也可以不必很冗长,但需要多消耗一点(完全可以忽略的)内存,如下面所示:
var _ = map[bool]int{false: 0, N>=M: 1}
类似地,下面是断言两个整数常量相等的方法:
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} // 唯一被允许的元素索引下标为0
var _ = [1]int{}[M-N] // 唯一被允许的元素索引下标为0
var _ [N-M]int = [M-N]int{}
最后一行的灵感同样来自于Luke Champine的一条tweet。
下面是一些用来断言一个常量字符串是不是一个空串的方法。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的一个点子。
有时候,为了避免包级变量消耗太多的内存,我们可以把断言代码放在一个名为空标识符的函数体中。 例如:func _() {
var _ = map[bool]int{false: 0, N>=M: 1}
var _ [N-M]int
}
const MaxUint = ^uint(0)
const MaxInt = int(^uint(0) >> 1)
const Is64bitArch = ^uint(0) >> 63 == 1
const Is32bitArch = ^uint(0) >> 63 == 0
const WordBits = 32 << (^uint(0) >> 63) // 64或32
当一个非接口值被赋值给一个接口值时,此非接口值的一个副本将被包裹到此接口值中。 副本复制的开销和非接口值的尺寸成正比。尺寸越大,复制开销越大。 所以请尽量避免将大尺寸的值包裹到接口值中。
在下面的例子中,后两个打印调用的成本要比前两个低得多。package main
import "fmt"
func main() {
var a [1000]int
// 这两行的开销相对较大,因为数组a中的元素都将被复制。
fmt.Println(a)
fmt.Printf("Type of a: %T\n", a)
// 这两行的开销较小,数组a中的元素没有被复制。
fmt.Printf("%v\n", a[:])
fmt.Println("Type of a:", fmt.Sprintf("%T", &a)[1:])
}
关于不同种类的类型的尺寸,请阅读值复制成本一文。
请阅读此文来获知什么是边界检查消除(BCE)以及目前的标准编译器对BCE的支持程度。
下面是一个利用了BCE进行性能优化的例子: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)
}
}
从下面所示的基准测试结果来看,函数NumSameBytes_2
比函数NumSameBytes_1
效率更高。
BenchmarkNumSameBytes_1-4 10000000 669 ns/op
BenchmarkNumSameBytes_2-4 20000000 450 ns/op
请注意:标准编译器(gc)的每个新的主版本都会有很多小的改进。
上例中所示的优化从gc 1.11开始才有效。
未来的gc版本可能会变得更加智能,以使函数NumSameBytes_2
中使用技巧变得不再必要。
事实上,从gc 1.11开始,如果x
和y
是两个切片,即使上例中使用小技巧没有被使用,y[i]
中的边界检查也已经被消除了。
Go101.org网站内容包括Go编程各种相关知识(比如Go基础、Go优化、Go细节、Go实战、Go测验、Go工具等)。后续将不断有新的内容加入。敬请收藏关注期待。
本丛书微信公众号(联系方式一)名称为"Go 101"。二维码在网站首页。此公众号将时不时地发表一些Go语言相关的原创短文。各位如果感兴趣,可以搜索关注一下。
《Go语言101》系列丛书项目目前托管在Github上(联系方式二)。欢迎各位在此项目中通过提交bug和PR的方式来改进完善《Go语言101》丛书中的各篇文章。我们可以在项目目录下运行go run .
来浏览和确认各种改动。
本书的twitter帐号为@Golang_101(联系方式三)。玩推的Go友可以适当关注。
你或许对本书作者老貘开发的一些App感兴趣。
sync
标准库包sync/atomic
标准库包