企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
1、使用指针接收方法的值的实例 只要值是可取址的,那在这个值上调用指针接收方法是没问题的。换句话说,在某些情况下,你不需要在有一个接收值的方法版本。 然而并不是所有的变量是可取址的。Map的元素就不是。通过interface引用的变量也不是。 错误代码: ~~~ package main import "fmt" type data struct { name string } func (p *data) print() { fmt.Println("name:", p.name) } type printer interface { print() } func main() { d1 := data{"one"} d1.print() //ok var in printer = data{"two"} //error in.print() m := map[string]data{"x": data{"three"}} m["x"].print() //error } ~~~ 编译错误: ~~~ ./main.go:20:6: cannot use data literal (type data) as type printer in assignment: data does not implement printer (print method has pointer receiver) ./main.go:23:8: cannot call pointer method on m["x"] ./main.go:23:8: cannot take the address of m["x"] ~~~ 2、更新Map的值 如果你有一个struct值的map,你无法更新单个的struct值。 ~~~ package main type data struct { name string } func main() { m := map[string]data{"x": {"one"}} m["x"].name = "two" //error } ~~~ 编译错误: ~~~ ./main.go:9:14: cannot assign to struct field m["x"].name in map ~~~ 这个操作无效是因为map元素是无法取址的。 而让Go新手更加困惑的是slice元素是可以取址的。 ~~~ package main import "fmt" type data struct { name string } func main() { one := data{"one"} s := []data{one} s[0].name = "two" //ok fmt.Println(s) //prints: [{two}] } ~~~ 运行结果: ~~~ [{two}] ~~~ 注意在不久之前,使用编译器之一(gccgo)是可以更新map的元素值的,但这一行为很快就被修复了 它也被认为是Go 1.3的潜在特性。在那时还不是要急需支持的,但依旧在todo list中。 第一个有效的方法是使用一个临时变量。 ~~~ package main import "fmt" type data struct { name string } func main() { m := map[string]data{"x": {"one"}} r := m["x"] r.name = "two" m["x"] = r fmt.Printf("%v\n", m) //prints: map[x:{two}] } ~~~ 运行结果: ~~~ map[x:{two}] ~~~ 另一个有效的方法是使用指针的map。 ~~~ package main import "fmt" type data struct { name string } func main() { m := map[string]*data{"x": {"one"}} m["x"].name = "two" //ok fmt.Println(m["x"]) //prints: &{two} } ~~~ 运行结果: ~~~ &{two} ~~~ 顺便说下,当你运行下面的代码时会发生什么? ~~~ package main type data struct { name string } func main() { m := map[string]*data{"x": {"one"}} m["z"].name = "what?" //??? } ~~~ 运行错误: ~~~ panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0x104f9af] ~~~ 3、"nil" Interfaces和"nil" Interfaces的值 这在Go中是第二最常见的技巧,因为interface虽然看起来像指针,但并不是指针。interface变量仅在类型和值为“nil”时才为“nil”。 interface的类型和值会根据用于创建对应interface变量的类型和值的变化而变化。当你检查一个interface变量是否等于“nil”时,这就会导致未预期的行为。 ~~~ package main import "fmt" func main() { var data *byte var in interface{} fmt.Println(data, data == nil) //prints: <nil> true fmt.Println(in, in == nil) //prints: <nil> true in = data fmt.Println(in, in == nil) //prints: <nil> false //'data' is 'nil', but 'in' is not 'nil' } ~~~ 运行结果: ~~~ <nil> true <nil> true <nil> false ~~~ 当你的函数返回interface时,小心这个陷阱。 ~~~ package main import "fmt" func main() { doit := func(arg int) interface{} { var result *struct{} = nil if arg > 0 { result = &struct{}{} } return result } if res := doit(-1); res != nil { fmt.Println("good result:", res) //prints: good result: <nil> //'res' is not 'nil', but its value is 'nil' } } ~~~ 运行结果: ~~~ good result: <nil> ~~~ ~~~ package main import "fmt" func main() { doit := func(arg int) interface{} { var result *struct{} = nil if arg > 0 { result = &struct{}{} } else { return nil } return result } if res := doit(-1); res != nil { fmt.Println("good result:", res) } } ~~~ 4、栈和堆变量 你并不总是知道变量是分配到栈还是堆上。在C++中,使用new创建的变量总是在堆上。在Go中,即使是使用new()或者make()函数来分配,变量的位置还是由编译器决定。编译器根据变量的大小和“泄露分析”的结果来决定其位置。这也意味着在局部变量上返回引用是没问题的,而这在C或者C++这样的语言中是不行的。 如果你想知道变量分配的位置,在“go build”或“go run”上传入“-m“ gc标志(即,go run -gcflags -m app.go)。 5、GOMAXPROCS, 并发, 和并行 默认情况下,Go仅使用一个执行上下文/OS线程(在当前的版本)。这个数量可以通过设置GOMAXPROCS来提高。 一个常见的误解是,GOMAXPROCS表示了CPU的数量,Go将使用这个数量来运行goroutine。而runtime.GOMAXPROCS()函数的文档让人更加的迷茫。GOMAXPROCS变量描述(https://golang.org/pkg/runtime/)所讨论OS线程的内容比较好。 你可以设置GOMAXPROCS的数量大于CPU的数量。 ~~~ package main import ( "fmt" "runtime" ) func main() { fmt.Println(runtime.GOMAXPROCS(-1)) fmt.Println(runtime.NumCPU()) runtime.GOMAXPROCS(20) fmt.Println(runtime.GOMAXPROCS(-1)) runtime.GOMAXPROCS(300) fmt.Println(runtime.GOMAXPROCS(-1)) } ~~~ 运行结果: ~~~ 4 4 20 300 ~~~ 6、读写操作的重排顺序 Go可能会对某些操作进行重新排序,但它能保证在一个goroutine内的所有行为顺序是不变的。然而,它并不保证多goroutine的执行顺序。 package main import ( "runtime" "time" ) var _ = runtime.GOMAXPROCS(3) var a, b int func u1() { a = 1 b = 2 } func u2() { a = 3 b = 4 } func p() { println(a) println(b) } func main() { go u1() go u2() go p() time.Sleep(1 * time.Second) } 如果你多运行几次上面的代码,你可能会发现a和b变量有多个不同的组合: a和b最有趣的组合式是"02"。这表明b在a之前更新了。 如果你需要在多goroutine内放置读写顺序的变化,你将需要使用channel,或者使用"sync"包构建合适的结构体。 7、优先调度 有可能会出现这种情况,一个无耻的goroutine阻止其他goroutine运行。当你有一个不让调度器运行的for循环时,这就会发生。 ~~~ package main import "fmt" func main() { done := false go func() { done = true }() for !done { } fmt.Println("done!") } ~~~ for循环并不需要是空的。只要它包含了不会触发调度执行的代码,就会发生这种问题。 调度器会在GC、“go”声明、阻塞channel操作、阻塞系统调用和lock操作后运行。它也会在非内联函数调用后执行。 ~~~ package main import "fmt" func main() { done := false go func() { done = true }() for !done { fmt.Println("not done!") //not inlined } fmt.Println("done!") } ~~~ 要想知道你在for循环中调用的函数是否是内联的,你可以在“go build”或“go run”时传入“-m” gc标志(如, go build -gcflags -m)。 另一个选择是显式的唤起调度器。你可以使用“runtime”包中的 Goshed()函数。 ~~~ package main import ( "fmt" "runtime" ) func main() { done := false go func() { done = true }() for !done { runtime.Gosched() } fmt.Println("done!") } ~~~