Blog

接口完整性检查

接口完整性检查 #

Go 语言的编译器并没有严格检查一个对象是否实现了某接口所有的接口方法,如下面这个示例:

type Shape interface {
Sides() int
Area() int
}
type Square struct {
len int
}
func (s* Square) Sides() int {
return 4
}
func main() {
s := Square{len: 5}
fmt.Printf("%d\n",s.Sides())
}

可以看到,Square 并没有实现 Shape 接口的所有方法,程序虽然可以跑通,但是这样的编程方式并不严谨,如果我们需要强制实现接口的所有方法,那该怎么办呢?

在 Go 语言编程圈里,有一个比较标准的做法:

var _ Shape = (*Square)(nil)

声明一个 _ 变量(没人用)会把一个 nil 的空指针从 Square 转成 Shape,这样,如果没有实现完相关的接口方法,编译器就会报错:

cannot use (*Square)(nil) (type *Square) as type Shape in assignment: *Square does not implement Shape (missing Area method)

这样就做到了强验证的方法。

Viewpoint #

From #

107 | Go编程模式:切片、接口、时间和性能

深度比较

深度比较 #

当我们复制一个对象时,这个对象可以是内建数据类型、数组、结构体、 Map……在复制结构体的时候,如果我们需要比较两个结构体中的数据是否相同,就要使用深度比较,而不只是简单地做浅度比较。这里需要使用到反射 reflect.DeepEqual() ,下面是几个示例:

import (
    "fmt"
    "reflect"
)

func main() {

    v1 := data{}
    v2 := data{}
    fmt.Println("v1 == v2:",reflect.DeepEqual(v1,v2))
    //prints: v1 == v2: true

    m1 := map[string]string{"one": "a","two": "b"}
    m2 := map[string]string{"two": "b", "one": "a"}
    fmt.Println("m1 == m2:",reflect.DeepEqual(m1, m2))
    //prints: m1 == m2: true

    s1 := []int{1, 2, 3}
    s2 := []int{1, 2, 3}
    fmt.Println("s1 == s2:",reflect.DeepEqual(s1, s2))
    //prints: s1 == s2: true
}

**

Viewpoint #

From #

107 | Go编程模式:切片、接口、时间和性能

Full Slice Expression

Content #

我们再来看一个例子:

func main() {
    path := []byte("AAAA/BBBBBBBBB")
    sepIndex := bytes.IndexByte(path,'/')

    dir1 := path[:sepIndex]
    dir2 := path[sepIndex+1:]

    fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAA
    fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => BBBBBBBBB

    dir1 = append(dir1,"suffix"...)

    fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAAsuffix
    fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => uffixBBBB
}

在这个例子中,dir1 和 dir2 共享内存,虽然 dir1 有一个 append() 操作,但是因为 cap 足够,于是数据扩展到了dir2 的空间。下面是相关的图示(注意上图中 dir1 和 dir2 结构体中的 cap 和 len 的变化):

如果要解决这个问题,我们只需要修改一行代码。我们要把代码

dir1 := path[:sepIndex]

修改为:

dir1 := path[:sepIndex:sepIndex]

新的代码使用了 Full Slice Expression,最后一个参数叫“Limited Capacity”,于是,后续的 append() 操作会导致重新分配内存。

...

中间件应用模式

中间件应用模式 #

中间件(Middleware)这个词的含义可大可小。在 Go Web 编程中,“中间件”常常指的是一个实现了 http.Handler 接口的 http.HandlerFunc 类型实例。实质上,这里的中间件就是包装模式和适配器模式结合的产物。

我们来看一个例子:

func validateAuth(s string) error {
    if s != "123456" {
        return fmt.Errorf("%s", "bad auth token")
    }
    return nil
}

func greetings(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome!")
}

func logHandler(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        t := time.Now()
        log.Printf("[%s] %q %v\n", r.Method, r.URL.String(), t)
        h.ServeHTTP(w, r)
    })
}

func authHandler(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        err := validateAuth(r.Header.Get("auth"))
        if err != nil {
            http.Error(w, "bad auth param", http.StatusUnauthorized)
            return
        }
        h.ServeHTTP(w, r)
    })

}

func main() {
    http.ListenAndServe(":8080", logHandler(authHandler(http.HandlerFunc(greetings))))
}

所谓中间件(如:logHandler、authHandler)本质就是一个包装函数(支持链式调用),但它的内部利用了适配器函数类型(http.HandlerFunc),将一个普通函数(比如例子中的几个匿名函数)转型为实现了 http.Handler 的类型的实例。

运行这个示例,并用 curl 工具命令对其进行测试,我们可以得到下面结果:

...

适配器函数类型(http.HandlerFunc)

适配器函数类型 #

适配器模式的核心是适配器函数类型(Adapter Function Type)。适配器函数类型是一个辅助水平组合实现的“工具”类型。这里我要再强调一下,它是一个类型。它可以将一个满足特定函数签名的普通函数,显式转换成自身类型的实例,转换后的实例同时也是某个接口类型的实现者。

最典型的适配器函数类型莫过于http.HandlerFunc了。这里,我们再来看一个应用 http.HandlerFunc 的例子:

func greetings(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome!")
}

func main() {
    http.ListenAndServe(":8080", http.HandlerFunc(greetings))
}

这个例子通过 http.HandlerFunc 这个适配器函数类型,将普通函数 greetings 快速转化为满足 http.Handler 接口的类型。而 http.HandleFunc 这个适配器函数类型的定义是这样的:

// $GOROOT/src/net/http/server.go
func ListenAndServe(addr string, handler Handler) error

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

经过 HandlerFunc 的适配转化后,我们就可以将它的实例用作实参,传递给接收 http.Handler 接口的 http.ListenAndServe 函数,从而实现基于接口的组合。

Viewpoint #

From #

30|接口:Go中最强大的魔法

包装器模式

包装器模式 #

在基本模式的基础上,当返回值的类型与参数类型相同时,我们能得到下面形式的函数原型:

func YourWrapperFunc(param YourInterfaceType) YourInterfaceType

通过这个函数,我们可以实现对输入参数的类型的包装,并在不改变被包装类型(输入参数类型)的定义的情况下,返回具备新功能特性的、实现相同接口类型的新类型。这种接口应用模式我们叫它包装器模式,也叫装饰器模式。包装器多用于对输入数据的过滤、变换等操作。

下面就是 Go 标准库中一个典型的包装器模式的应用:

// $GOROOT/src/io/io.go
func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} }

type LimitedReader struct {
    R Reader // underlying reader
    N int64  // max bytes remaining
}

func (l *LimitedReader) Read(p []byte) (n int, err error) {
    // ... ...
}

通过上面的代码,我们可以看到,通过 LimitReader 函数的包装后,我们得到了一个具有新功能特性的 io.Reader 接口的实现类型,也就是 LimitedReader。这个新类型在 Reader 的语义基础上实现了对读取字节个数的限制。

接下来我们再具体看 LimitReader 的一个使用示例:

func main() {
    r := strings.NewReader("hello, gopher!\n")
    lr := io.LimitReader(r, 4)
    if _, err := io.Copy(os.Stdout, lr); err != nil {
        log.Fatal(err)
    }
}

运行这个示例,我们得到了这个结果:

hell

当采用经过 LimitReader 包装后返回的 io.Reader 去读取内容时,读到的是经过 LimitedReader 约束后的内容,也就是只读到了原字符串前面的 4 个字节:“hell”。

由于包装器模式下的包装函数(如上面的 LimitReader)的返回值类型与参数类型相同,因此我们可以将多个接受同一接口类型参数的包装函数组合成一条链来调用,形式是这样的:

...

接受接口返回结构体

创建模式 #

Go 社区流传一个经验法则:“接受接口,返回结构体(Accept interfaces, return structs)”,这其实就是一种把接口作为“关节”的应用模式。我这里把它叫做创建模式,是因为这个经验法则多用于创建某一结构体类型的实例。

下面是 Go 标准库中,运用创建模式创建结构体实例的代码摘录:

// $GOROOT/src/sync/cond.go
type Cond struct {
    ... ...
    L Locker
}

func NewCond(l Locker) *Cond {
    return &Cond{L: l}
}

// $GOROOT/src/log/log.go
type Logger struct {
    mu     sync.Mutex
    prefix string
    flag   int
    out    io.Writer
    buf    []byte
}

func New(out io.Writer, prefix string, flag int) *Logger {
    return &Logger{out: out, prefix: prefix, flag: flag}
}

// $GOROOT/src/log/log.go
type Writer struct {
    err error
    buf []byte
    n   int
    wr  io.Writer
}

func NewWriterSize(w io.Writer, size int) *Writer {
    // Is it already a Writer?
    b, ok := w.(*Writer)
    if ok && len(b.buf) >= size {
        return b
    }
    if size <= 0 {
        size = defaultBufSize
    }
    return &Writer{
        buf: make([]byte, size),
        wr:  w,
    }
}

创建模式在 sync、log、bufio 包中都有应用。以上面 log 包的 New 函数为例,这个函数用于实例化一个 log.Logger 实例,它接受一个 io.Writer 接口类型的参数,返回 *log.Logger。从 New 的实现上来看,传入的 out 参数被作为初值赋值给了 log.Logger 结构体字段 out。

...

Go语言提供的正交性

Content #

编程语言的语法元素间和语言特性也存在着正交的情况,并且通过将这些正交的特性组合起来,我们可以实现更为高级的特性。在语言设计层面,Go 语言就为广大 Gopher 提供了诸多正交的语法元素供后续组合使用,包括:

  1. Go 语言无类型体系(Type Hierarchy),没有父子类的概念,类型定义是正交独立的;
  2. 方法和类型是正交的,每种类型都可以拥有自己的方法集合,方法本质上只是一个将 receiver 参数作为第一个参数的函数而已;
  3. 接口与它的实现者之间无“显式关联”,也就说接口与 Go 语言其他部分也是正交的。

Viewpoint #

From #

30|接口:Go中最强大的魔法

mcentral结构

Content #

当 mcache 中的内存不够需要扩容时,需要向 mcentral 请求,mcentral 对应于 TCMalloc 中的 Central cache 结构。mcentral 的主要结构如下:

type mcentral struct {
    spanclass spanClass
    partial [2]spanSet // list of spans with a free object
    full    [2]spanSet // list of spans with no free objects
}

mcentral 中也存有 spanClass 的 ID 标识符,这表示说每个 mcentral 维护着固定一种 spanClass 的 mspan。 spanClass 下面是两个 spanSet,它们是 mcentral 维护的 mspan 集合。 partial 里存放的是包含着空闲空间的 mspan 集合,full 里存放的是不包含空闲空间的 span 集合。这里每种集合都存放两个元素,用来区分集合中 mspan 是否被清理过。

mcentral 不同于 mcache,每次请求 mcentral 中的 mspan 时,都可能发生不同线程直接的竞争。因此,在使用 mcentral 时需要进行加锁访问,具体来讲,就是 spanSet 的结构中会有一个 mutex 的锁的字段。

...

nil error 值 != nil

nil error 值 != nil #

我们直接来看一段改编自 Go FAQ中的例子的代码:

type MyError struct {
    error
}
var ErrBad = MyError{
    error: errors.New("bad things happened"),
}
func bad() bool {
    return false
}
func returnsError() error {
    var p *MyError = nil
    if bad() {
        p = &ErrBad
    }
    return p
}
func main() {
    err := returnsError()
    if err != nil {
        fmt.Printf("error occur: %+v\n", err)
        return
    }
    fmt.Println("ok")
}

returnsError 这个函数里定义了一个*MyError类型的变量 p,初值为 nil。如果函数 bad 返回 false,returnsError 函数就会直接将 p(此时 p = nil)作为返回值返回给调用者,之后调用者会将 returnsError 函数的返回值(error 接口类型)与 nil 进行比较,并根据比较结果做出最终处理。

如果你是一个初学者,我猜你的的思路大概是这样的:p 为 nil,returnsError 返回 p,那么 main 函数中的 err 就等于 nil,于是程序输出 ok 后退出。但真实的运行结果是什么样的呢?我们来看一下:

...