内容简介:使用 Go 已经一年,深深沉浸在其简洁的设计中,就像其官网描述的:Rob Pike 在Go 的设计目标是取代 C/C++,所以 Go 里面的 struct 和 C 的类似,与 int/float 一样属于
使用 Go 已经一年,深深沉浸在其简洁的设计中,就像其官网描述的:
Go is expressive, concise, clean, and efficient. It’s a fast, statically typed, compiled language that feels like a dynamically typed, interpreted language.
Rob Pike 在 Simplicity is Complicated 中也提到 Go 的简洁是其流行的重要原因。简洁并不意味着简单,Go 有着诸多设计确保了把复杂性隐藏在背后。本文就结合笔者自身经验,来讨论 Go 中 struct/interface 的设计理念与最佳实践,帮助读者写出健壮、高效的 Go 程序。
值类型的 struct
Go 的设计目标是取代 C/C++,所以 Go 里面的 struct 和 C 的类似,与 int/float 一样属于 值类型 ,值类型最重要的特点是在进行赋值时,新变量会得到一份拷贝后的值,这和 Java 中以引用赋值的 Object 有着本质区别。
这意味着,如果要改变 struct 的内部状态,需要将其定义为指针类型 *struct 。
type student struct {
name string
}
foo := student{name: "foo"}
bar := foo
bar.name = "bar"
fmt.Println(foo.name) // 输出 foo
bar2 := &foo
bar2.name = "bar"
fmt.Println(foo.name) // 输出 bar
与之类似的,使用 for range 遍历 []struct map[xx]struct 时,得到的也是一份拷贝。
m := map[int]student{
1: {name: "1"},
}
m[1].name = "2" // 编译错误: cannot assign to struct field m[1].name in map
可以看到,无法直接对 map 中的 struct 进行赋值,这是由于这里的赋值操作是个 read-modify-write 操作,无法保证原子性,所以目前版本的 Go 直接不支持这种操作,可参考 #3117 。
笔者多次遇到这个“坑”,那是不是说把所有的 struct 都定义为指针就好了呢?这里需要了解下 Go 的逃逸分析才能回答这个问题。
逃逸分析
逃逸分析的主要作用是决定对象分配在内存中的位置,Go 会尽量分配在 stack 上,这样的好处显而易见:回收简单,减轻 GC 压力。可以通过 go build -gcflags -m xx.go 查看
func returnByValue(name string) student {
return student{name}
}
func returnByPointer(name string) *student {
return &student{name}
}
./snippet.go:6:18: &student literal escapes to heap
可以看到, returnByPointer 方法的返回值会逃逸,最终分配在 heap 上,关于变量分配在 stack / heap 上的性能差距,可参考: github gist 、 gitee
// snippet.go
package main
import (
"fmt"
)
type student struct {
name string
}
//go:noinline
func (s student) getNameByValue() string {
return s.name
}
//go:noinline
func (s *student) getNameByPointer() string {
return s.name
}
const randStr = "a very long string,a very long string,a very long string,a very long string"
//go:noinline
func returnByValue() student {
return student{randStr}
}
//go:noinline
func returnByPointer() *student {
return &student{randStr}
}
// bench_test.go
package main
import "testing"
var blackholeStr = ""
var blackholeValue student
var blackholePointer *student
func BenchmarkPointerVSStruct(b *testing.B) {
b.Run("return pointer", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
blackholePointer = returnByPointer()
}
})
b.Run("return value", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
blackholeValue = returnByValue()
}
})
b.Run("value receiver", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
r := student{
name: randStr,
}
blackholeStr = r.getNameByValue()
}
})
b.Run("pointer receiver", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
r := &student{
name: randStr,
}
blackholeStr = r.getNameByPointer()
}
})
}
测试结果:
go test -run ^NOTHING -bench Struct bench_test.go snippet.go goos: darwin goarch: amd64 BenchmarkPointerVSStruct/return_pointer-8 34476903 32.4 ns/op 16 B/op 1 allocs/op BenchmarkPointerVSStruct/return__value-8 530538498 2.27 ns/op 0 B/op 0 allocs/op BenchmarkPointerVSStruct/value_receiver-8 415309486 2.86 ns/op 0 B/op 0 allocs/op BenchmarkPointerVSStruct/pointer_receiver-8 348904872 3.23 ns/op 0 B/op 0 allocs/op PASS ok command-line-arguments 5.699s
可以看到,方法返回 pointer 时,会有一次 heap 分配,占 16 个字节,这正好是 name 字段(string 类型)的大小,8 个字节表示指向数据的指针,8 个字节表示长度(笔者为 64 位系统),类似下面的结构
type StringHeader struct {
Data uintptr
Len int
}
方法返回 value 时,则没有 heap 分配,说明所有变量都分配在 stack 上。
对于 receiver 为 pointer 或 value 性能则无差别,这是因为 s 在两种情况下均无逃逸,所以都分配在了 stack 上,这也说明变量分配在那里与是否为指针无关。
value vs pointer
结合上面的实验,可以按照下述流程确定选用 value/pointer:
unsafe.Sizeof(struct)
为了确定出 2 中的阈值,可以在 struct 中添加一数组元素,之后再来跑上述测试即可,在笔者机器中,这个阈值大概为 72K,很少有 struct 会达到这个量级,这是由于 Go 中常用的 slice/map/string 均为复合类型(可认为由 header+data 两部分组成),在 struct 的结构中,只保存 header 部分,所以大小是固定的,而 array 有的地方也不是很多,所以读者可认为只要 struct 状态不需要改变,value 则是最佳选择。
type student struct {
name string
dummy [9000]int64 // 添加一数组元素
}
BenchmarkPointerVSStruct/return_pointer-8 150147 8147 ns/op 73728 B/op 1 allocs/op
BenchmarkPointerVSStruct/return__value-8 138591 8146 ns/op 0 B/op 0 allocs/op
| 简单类型 | 复合类型 |
|---|---|
| bool | slice |
| numeric | map |
| (unsafe)pointer | channel |
| struct | function |
| array | interface |
| string |
map[string]uint64{
"ptr": uint64(unsafe.Sizeof(&struct{}{})),
"map": uint64(unsafe.Sizeof(map[bool]bool{})),
"slice": uint64(unsafe.Sizeof([]struct{}{})),
"chan": uint64(unsafe.Sizeof(make(chan struct{}))),
"func": uint64(unsafe.Sizeof(func() {})),
"interface": uint64(unsafe.Sizeof(interface{}(0))),
}
// 输出
map[chan:8 func:8 interface:16 map:8 ptr:8 slice:24]
可以看到,
- chan/func/map/ptr 均为 8 个字节,即一个指向具体数据的指针
- interface 为 16,两个指针,一个指向具体类型,一个指向具体数据。细节可参考 Russ Cox 的 Go Data Structures: Interfaces
- slice 为 24,包括一个指向底层 array 的指针,两个整型,分布表示 cap、len
内存对齐
struct 中的字段会按照机器字长进行对齐,所以在性能要求比较高的地方,可以尽量把相同类型的字段放一起。
fmt.Println(
unsafe.Sizeof(struct {
a bool
b string
c bool
}{}),
unsafe.Sizeof(struct {
a bool
c bool
b string
}{}),
)
上述代码会依次输出 32 24 ,下面的图示清晰的展示了两个顺序的 struct 在内存中的布局:( 图片来源 )
基于组合的 interface
如果说 struct 是对状态的封装,那么 interface 就是对行为的封装,是 Go 中构造抽象的基础。由于 Go 中没有 oop 的概念,主要是通过组合,而非继承来实现不同组件的整合,比如 io 包下的 Reader/Writer。
但就组合来说,并没有什么优势,Java 中也可以实现,但 Go 中的隐式“继承” 让组合变得十分灵活。
Embedded struct
下面通过一示例进行说明:
type RecordWriter struct {
code int
http.ResponseWriter
}
func (rw *RecordWriter) WriteHeader(statusCode int) {
rw.code = statusCode
rw.ResponseWriter.WriteHeader(statusCode)
}
func URLStat(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
// if w.WriteHeader isn't called inside handlerFunc, 200 is the default code.
rw := &RecordWriter{ResponseWriter: w, code: 200}
next(rw, r)
metrics.HTTPReqs.WithLabelValues(r.URL.Path, r.Method, strconv.FormatInt(int64(rw.code), 10)).Inc()
}
上述代码片段为 negroni 中的一个 middleware,用来记录 http code。自定义 Writer 通过嵌入 ResponseWriter,实现了 ResponseWriter 接口,然后通过重写 WriteHeader 的方式来实现业务需求,由于需要改变状态,所以采用指针类型 *RecordWriter 来作为 receiver,整个实现非常简洁扼要。
New func type
第二个示例是关于如何通过自定义 type,来达到简化 err 处理的目的。在 net/http 中,handlerFunc 没有返回值,这就导致在每个异常处理的后面加上一个空的 return 来中止逻辑处理,这样不仅繁琐,还容易遗漏,
func viewRecord(w http.ResponseWriter, r *http.Request) {
c := appengine.NewContext(r)
key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
record := new(Record)
if err := datastore.Get(c, key, record); err != nil {
http.Error(w, err.Error(), 500)
return
}
if err := viewTemplate.Execute(w, record); err != nil {
http.Error(w, err.Error(), 500)
}
}
这时便可通过自定义新类型来解决这个问题:
type appError struct {
Error error
Message string
Code int
}
type appHandler func(http.ResponseWriter, *http.Request) appError
func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if e := fn(w, r); e != nil { // e is *appError, not os.Error.
c := appengine.NewContext(r)
c.Errorf("%v", e.Error)
http.Error(w, e.Message, e.Code)
}
}
func viewRecord(w http.ResponseWriter, r *http.Request) appError {
c := appengine.NewContext(r)
key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
record := new(Record)
if err := datastore.Get(c, key, record); err != nil {
return appError{err, "Record not found", 404}
}
if err := viewTemplate.Execute(w, record); err != nil {
return appError{err, "Can't display record", 500}
}
return appError{}
}
mux.HandleFunc("/view", appHandler(viewRecord))
可以看到,上述示例通过定义 appHandler 新函数类型,并隐式“继承” http.Handler 接口来达到了统一集中处理 err 的需求。
该实现漂亮的地方为函数增加新类型,且函数签名与 ServeHTTP 一致,这样就可以直接复用参数。对于初学者来说,可能没想到也可以给 func 类型来定义方法,但是在 Go 中,可以给任何类型增加方法。
之前在网上看到一些框架,采用 panic 的方式来简化 err 处理,感觉这属于对 panic 的滥用,先不说对性能是否有损耗,更主要的是破坏了 if err != nil 的处理方式。希望读者在后续处理繁琐的逻辑时,多去考虑如何抽象新类型来解决。
总结
Go 的精妙设计保证了其简洁的特性,而且这些特性可能和传统的 oop 不同,这对于从这些语言转过来的读者来说会采用旧思维去思考问题,这无可厚非,但作为优秀的 Go 程序员,更多的需要从 Go 自身特点来考虑问题,这样就不至于产生“为什么 XX 特性在 Go 中没有”的疑惑,要知道 Go 的作者可是 Rob Pike, Ken Thompson :-)
如果读者阅读/实现过基于 interface 的精巧设计,欢迎留言分享。
参考
以上所述就是小编给大家介绍的《Go struct/interface 最佳实践》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- vue项目实践004~~~一篮子的实践技巧
- HBase实践 | 阿里云HBase数据安全实践
- Spark 实践:物化视图在 SparkSQL 中的实践
- Spark实践|物化视图在 SparkSQL 中的实践
- HBase实践 | 数据人看Feed流-架构实践
- Kafka从上手到实践-实践真知:搭建Zookeeper集群
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Persuasive Technology
B.J. Fogg / Morgan Kaufmann / 2002-12 / USD 39.95
Can computers change what you think and do? Can they motivate you to stop smoking, persuade you to buy insurance, or convince you to join the Army? "Yes, they can," says Dr. B.J. Fogg, directo......一起来看看 《Persuasive Technology》 这本书的介绍吧!
SHA 加密
SHA 加密工具
html转js在线工具
html转js在线工具