go-kit学习笔记(一)

本文是学习go-kit的开篇,主要参考资料是官方文档。

在前面的一篇文章中,已经介绍了基于go开发api服务需要哪些步骤,那么我们将来实践这些步骤。

首先创建一个interface,

import "context"

type StringService interface {
        Uppercase(context.Context, string) (string, error)
        Count(context.Context, string) int
}

然后编写代码实现这个interface。

import (
        "context"
        "errors"
        "strings"
)

type stringService struct{}

func (stringService) Uppercase(_ context.Context, s string) (string, error) {
        if s == "" {
                return "", ErrEmpty
        }
        return strings.ToUpper(s), nil
}

func (stringService) Count(_ context.Context, s string) int {
        return len(s)
}

var ErrEmpty = errors.New("Empty string”)

在go-kit中首选的消息机制是RPC,所以我们接下来定义request和response的结构,保存输入和输出的数据。

type uppercaseRequest struct {
        S string `json:"s"`
}

type uppercaseResponse struct {
        V   string `json:"v"`
        Err string `json:"err,omitempty"` // errors don't JSON-marshal, so we use a string
}

type countRequest struct {
        S string `json:"s"`
}

type countResponse struct {
        V int `json:"v"`
}

接下来就到了endpoint相关的逻辑了,在go-kit中,一个endpoint对应刚刚定义的interface中的方法,这里将要做的就是实现适配器将stringService中对应的方法转换成endpint.Endpoint类型。

import (
        "context"
        "github.com/go-kit/kit/endpoint"
)

func makeUppercaseEndpoint(svc StringService) endpoint.Endpoint {
        return func(ctx context.Context, request interface{}) (interface{}, error) {
                req := request.(uppercaseRequest)
                v, err := svc.Uppercase(ctx, req.S)
                if err != nil {
                        return uppercaseResponse{v, err.Error()}, nil
                }
                return uppercaseResponse{v, ""}, nil
        }
}

func makeCountEndpoint(svc StringService) endpoint.Endpoint {
        return func(ctx context.Context, request interface{}) (interface{}, error) {
                req := request.(countRequest)
                v := svc.Count(ctx, req.S)
                return countResponse{v}, nil
        }
}

接下来就到了transport层,这里我们需要将服务暴露出去,以便提供给外部调用,底层协议可以根据需要选择,这里采用http和json。

import (
        "context"
        "encoding/json"
        "log"
        "net/http"

        httptransport "github.com/go-kit/kit/transport/http"
)

func main() {
        svc := stringService{}

        uppercaseHandler := httptransport.NewServer(
                makeUppercaseEndpoint(svc),
                decodeUppercaseRequest,
                encodeResponse,
        )

        countHandler := httptransport.NewServer(
                makeCountEndpoint(svc),
                decodeCountRequest,
                encodeResponse,
        )

        http.Handle("/uppercase", uppercaseHandler)
        http.Handle("/count", countHandler)
        log.Fatal(http.ListenAndServe(":8080", nil))
}

func decodeUppercaseRequest(_ context.Context, r *http.Request) (interface{}, error) {
        var request uppercaseRequest
        if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
                return nil, err
        }
        return request, nil
}

func decodeCountRequest(_ context.Context, r *http.Request) (interface{}, error) {
        var request countRequest
        if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
                return nil, err
        }
        return request, nil
}

func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
        return json.NewEncoder(w).Encode(response)
}

至此,一个简单的基于go-kit的micro service已经全部开发完成了。

go-kit简介

gokit是什么

Gokit是一系列的工具的集合,能够帮助你快速的构建健壮的,可靠的,可维护的微服务。它提供了一系列构建微服务的成熟的模式和惯用法,背后有着一群经验丰富的开发者支持,并且已经在生产环境中被广泛的使用。

gokit的架构

gokit不同于传统的MVC的框架,它只是一系列工具的组合,他有着自己的层次结构,主要有三层,分别是transport,endpoint和service层。

transport层

这是一个抽象的层级,对应真实世界中的http/grpc/thrift等,通过gokit你可以在同一个微服务中同时支持http和grpc。

endpoint层

endpoint层对应于controller中的action,主要是实现安全逻辑的地方,如果你要同时支持http和grpc,那么你将需要创建两个方法同时路由到同一个endpoint。

service层

service是具体的业务逻辑实现的层级,在这里,你应该使用接口,并且通过实现这些接口来构建具体的业务逻辑。一个service通常聚合了多个endpoints,在service层,你应该使用clean architecture或者六边形模型,也就是说你的service层不需要知道enpoint以及transport层的具体实现,也不需要关心具体的http头部或者grpc的错误状态码。

middleware

middleware实现了装饰器模式,通过middleware你可以包装你的service或者endpoint,通常你需要构建一个middleware链来实现如日志,rate limit,负载均衡和分布式追踪。

缺点

  1. 太过复杂 如果你要添加一个API那么你将需要做如下的工作。

    1. 声明一个interface,并定义相关的方法
    2. 实现这个interface
    3. endpoint工厂方法
    4. transport方法
    5. request encoder,request decoder, response encoder response decoder
    6. 把endpoint添加到server
    7. 把endpoint添加到client
  2. 难以理解 各种分层,每一层都有特定的用处。

golang中context包详解

在开发go web服务器的时候,通常一个request是在特定的goroutine中完成,请求处理程序经常启动额外的goroutine来访问数据库或者RPC等后端服务,处理请求的一系列goroutine通常需要获取终端用户的标识,授权令牌以及确定请求什么时候终止。当请求终止的时候,这一系列的goroutine都应该被通知到并退出,以便系统能够回收资源。

context包就是为了在goroutine之间传递信息用的,能够使得在一系列的处理请求的goroutine中很方便的传递信息。

对服务器的传入请求应创建一个上下文,对服务器的传出调用应接受上下文。它们之间的函数调用链必须传播上下文,可以用使用WithCancel,WithDeadline,WithTimeout或WithValue创建的派生上下文替换它。当上下文被取消时,从它派生的所有上下文也被取消。

WithCancel,WithDeadline和WithTimeout函数采用Context(父级)并返回派生的Context(子级)和CancelFunc。调用CancelFunc将取消子对象及其子对象,删除父对子对象的引用,并停止任何关联的定时器。未能调用CancelFunc会泄漏子项及其子项,直至父项被取消或计时器激发。 go vet工具检查在所有控制流路径上使用CancelFuncs。

使用上下文的程序应该遵循这些规则来保持包之间的接口一致,并使静态分析工具能够检查上下文传播:

不要将上下文存储在结构类型中;相反,将一个Context明确地传递给每个需要它的函数。上下文应该是第一个参数,通常命名为ctx:

即使函数允许,也不要传递零上下文。如果您不确定要使用哪个上下文,请传递context.TODO。

使用上下文值仅适用于传输进程和API的请求范围数据,而不用于将可选参数传递给函数。

相同的上下文可以传递给在不同goroutine中运行的函数;上下文对于多个goroutine同时使用是安全的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//上下文包含截止期限,取消信号和请求范围值
//跨越API边界。它的方法对于多个同时使用是安全的
// goroutines。
type Context interface {
// 当一个Context被取消或者超时的时候返回一个关闭的通道
Done() <-chan struct{}

// 当Done通道关闭后,返回这个context被取消的原因
Err() error

// 当设置了取消时间的时候,返回什么时候被取消
Deadline() (deadline time.Time, ok bool)

// 返回键值对,如果没有返回nil
Value(key interface{}) interface{}
}

Done方法返回一个通道,该通道作为代表上下文运行的函数的取消信号:当通道关闭时,函数应放弃其工作并返回。 Err方法返回一个错误,指出Context被取消的原因。

由于与完成通道仅接收相同的原因,上下文没有取消方法:接收取消信号的功能通常不是发送信号的功能。特别是,当父操作为子操作启动子程序时,这些子操作不应该能够取消父操作。相反,WithCancel函数(如下所述)提供了一种方法来取消新的上下文值。

上下文对于多个goroutine同时使用是安全的。代码可以将单个Context传递给任意数量的goroutine,并取消该Context来发信号通知所有这些。

Deadline方法允许函数确定他们是否应该开始工作;如果剩下的时间太少,这可能不值得。代码也可以使用最后期限来设置I / O操作的超时时间。

值允许上下文携带请求范围的数据。该数据必须安全,可供多个goroutine同时使用。

WithCancel

1
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

WithCancel返回一个携带新的Done通道的父亲的副本。当返回的cancel方法被调用或者父级context的Done通道被关闭时,返回的context的Done通道会被关闭。

取消这个context会释放与它相关的资源,因此只要在这个Context中运行的操作完成,代码就应该立即调用cancel。

WithDeadline

1
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

WithDeadline返回父上下文的副本,并将截止日期调整为不晚于d。如果父母的截止日期早于d,WithDeadline(parent,d)在语义上等同于父母。当截止日期到期,返回的取消功能被调用时,或父上下文的完成通道关闭时,返回的上下文的完成通道关闭,以先发生者为准。

取消这个上下文会释放与它相关的资源,因此只要在这个Context中运行的操作完成,代码就应该立即调用cancel。

WithTimeout

1
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithTimeout返回WithDeadline(parent,time.Now()。Add(timeout))。

取消这个上下文可以释放与它相关的资源,因此只要在这个Context中运行的操作完成,代码就应该立即调用cancel

WithValue

1
func WithValue(parent Context, key, val interface{}) Context

WithValue返回父键的副本,其中与键关联的值是val。

使用上下文值仅适用于传输进程和API的请求范围数据,而不用于将可选参数传递给函数。

提供的密钥必须具有可比性,不应该是字符串类型或任何其他内置类型,以避免使用上下文的包之间发生冲突。 WithValue的用户应该为键定义他们自己的类型。为了避免在分配给接口时分配{},上下文键通常具有具体类型struct {}。或者,导出的上下文关键字变量的静态类型应该是指针或接口。

Golang中的Handle和HandleFunc

在golang的标准库文档中有这么一段示例:

1
2
3
4
5
6
7
http.Handle("/foo", fooHandler)

http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})

log.Fatal(http.ListenAndServe(":8080", nil))

Handle和HandleFunc都接收两个参数,第一个都是将要访问的路由,是一个string类型。不同的是第二个参数,前者接受一个实现了Handler interface的type,后者是一个Handler的方法。
之所以要有Handle是为了当逻辑比较复杂时可以在请求的过程中加入一些状态,一个实现了Handler类型的type可以很容易的做到这一点。但是这样做是很繁琐的,首先要定义一个类型,然后实现Handler接口,也就是编写
ServeHttp方法,试想以下,每次都要这样做要做很多额外的工作。所以golang标准库做了一层封装,也就是HandleFunc方法, 对于简单的场景,使用handleFunc开发效率更高一些。

在server.go的源码中可以看到handlerFunc的具体实现:

1
2
3
4
5
6
type HandlerFunc func(ResponseWriter, *Request)

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

首先自定义一个类型为func的type,添加一个ServeHttp的方法,并在方法体中调用自身,这实际上是一个wrapper,主要是为了方便。

Golang中的router

一个web应用程序要接受来及外部的url请求,一个重要的工作是对url进行解析,并将这次请求转给对应的逻辑代码进行处理,这里就是路由机制大展身手的地方了。

golang中有一个ServeMux类型,该类型同样实现了ServeHttp方法,因而可以直接作为参数传入http.LisenAndServe方法。iServeMux类型是HTTP请求的多路转接器。它会将每一个接收的请求的URL与一个注册模式的列表进行匹配,并调用和URL最匹配的模式的处理器。

下面来看一下具体的应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"io"
"net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "Hello, world!\n")
}

func echoHandler(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, r.URL.Path)
}

func main() {
mux := http.NewServeMux()
mux.HandleFunc("/hello", helloHandler)
mux.HandleFunc("/", echoHandler)

http.ListenAndServe(":8080", mux)
}

首先通过http.NewServeMux()创建并返回一个新的*ServeMux,然后相应的路由和handler都注册到它上面。
路由的匹配规则如下:

  1. 匹配根路径或者以根路径开始的子树,如’/‘和’/images/‘, 注意”/images/“后面的”/“,这代表一条子路径,可以匹配任何以”/images/“开始的路径,如果没有”/“则代表叶子,是一个固定的路径。
  2. 较长的路径匹配的优先级会高于较短的路径,如果同时注册了两个路由”/images/“和”/images/avatar”,请求的url是”http://localhost:8080/images/avatar/",那么会优先匹配后一条路由而不管这两条路由注册的先后顺序。
  3. 任何路径中包含”.”或”..”元素的请求重定向到等价的没有这两种元素的URL。

go自带的路由实现了一些基本的功能,但是并不完善:

  1. 没法处理query paramter如”/user/:id”
  2. 没法限定请求的http方法, 如”/user”, 用get,post,put,delete等都可以匹配到

    ServeMux源码

1
2
3
4
5
6
7
8
9
10
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
hosts bool // whether any patterns contain hostnames
}

type muxEntry struct {
h Handler
pattern string
}

首先定义ServeMux类型,是一个结构体,包括一个读写锁,一个路由注册器,和一个标示路由是否携带主机名的hosts bool变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Handle registers the handler for the given pattern.
// If a handler already exists for pattern, Handle panics.
func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.mu.Lock()
defer mux.mu.Unlock()

// 边界情况处理
if pattern == "" {
panic("http: invalid pattern")
}
if handler == nil {
panic("http: nil handler")
}
// 如果对应的路由已经注册,那么将触发panic
if _, exist := mux.m[pattern]; exist {
panic("http: multiple registrations for " + pattern)
}

// 如果还没有任何路由注册过,则创建一个map
if mux.m == nil {
mux.m = make(map[string]muxEntry)
}
// 将路由写入map
mux.m[pattern] = muxEntry{h: handler, pattern: pattern}

// 如果路由不以"/"开头,则说明有主机名
if pattern[0] != '/' {
mux.hosts = true
}
}

// HandleFunc封装Handle,处理方式和"net/http" 一致。
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
mux.Handle(pattern, HandlerFunc(handler))
}
1
2
3
4
5
6
7
8
9
10
11
12
// ServeHTTP将请求转发给最匹配的handler处理
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
if r.ProtoAtLeast(1, 1) {
w.Header().Set("Connection", "close")
}
w.WriteHeader(StatusBadRequest)
return
}
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Handler 返回根据路由匹配的Handler,
// 它永远返回一个非空的Handler,如果请求的路由是非标准化的,那么将会对其进行转换。
// 如果路由带有端口号,则在匹配的时候忽略。
//
// 如果是connect请求,则不会对host和path做处理。
// 如果没有匹配的,则返回""
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {

// CONNECT requests are not canonicalized.
if r.Method == "CONNECT" {
// If r.URL.Path is /tree and its handler is not registered,
// the /tree -> /tree/ redirect applies to CONNECT requests
// but the path canonicalization does not.
if u, ok := mux.redirectToPathSlash(r.URL.Host, r.URL.Path, r.URL); ok {
return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
}

return mux.handler(r.Host, r.URL.Path)
}

// 在交给mux.hanlder处理之前,先删除port,清理path
host := stripHostPort(r.Host)
path := cleanPath(r.URL.Path)

// 如果"/tree"没有注册,则返回一个带有3XX code的Handler,交给"/tree/"
if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok {
return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
}

// 如果处理的后的路径和原始路径不一致,交给RedirectHandler处理
if path != r.URL.Path {
_, pattern = mux.handler(host, path)
url := *r.URL
url.Path = path
return RedirectHandler(url.String(), StatusMovedPermanently), pattern
}

// 否则由mux.handler处理
return mux.handler(host, r.URL.Path)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// handler是Handler的主要处理逻辑,对path做标准化处理。
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
mux.mu.RLock()
defer mux.mu.RUnlock()

// 定义了hosts的路由有更高的优先级
if mux.hosts {
h, pattern = mux.match(host + path)
}
// 如果没有匹配到,则交给后续处理
if h == nil {
h, pattern = mux.match(path)
}
if h == nil {
h, pattern = NotFoundHandler(), ""
}
return
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// Return the canonical path for p, eliminating . and .. elements.
func cleanPath(p string) string {
if p == "" {
return "/"
}
if p[0] != '/' {
p = "/" + p
}
np := path.Clean(p)
// path.Clean removes trailing slash except for root;
// put the trailing slash back if necessary.
if p[len(p)-1] == '/' && np != "/" {
np += "/"
}
return np
}

// 删除port
func stripHostPort(h string) string {
// If no port on host, return unchanged
if strings.IndexByte(h, ':') == -1 {
return h
}
host, _, err := net.SplitHostPort(h)
if err != nil {
return h // on error, return unchanged
}
return host
}

// 路由越精确优先级越高
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
// Check for exact match first.
v, ok := mux.m[path]
if ok {
return v.h, v.pattern
}

// 找最长的路径
var n = 0
for k, v := range mux.m {
if !pathMatch(k, path) {
continue
}
if h == nil || len(k) > n {
n = len(k)
h = v.h
pattern = v.pattern
}
}
return
}

Golang逃逸分析

首先我们来看一下什么是逃逸分析

在编译程序优化理论中,逃逸分析是一种确定指针动态范围的方法——分析在程序的哪些地方可以访问到指针。它涉及到指针分析和形状分析。 当一个变量(或对象)在子程序中被分配时,一个指向变量的指针可能逃逸到其它执行线程中,或者去调用子程序。如果使用尾递归优化(通常在函数编程语言中是需要的),对象也可能逃逸到被调用的子程序中。如果一种语言支持第一类型的延续性在Scheme和Standard ML of New Jersey中同样如此),部分调用栈也可能发生逃逸。 如果一个子程序分配一个对象并返回一个该对象的指针,该对象可能在程序中的任何一个地方被访问到——这样指针就成功“逃逸”了。如果指针存储在全局变量或者其它数据结构中,它们也可能发生逃逸,这种情况是当前程序中的指针逃逸。 逃逸分析需要确定指针所有可以存储的地方,保证指针的生命周期只在当前进程或线程中。

在Go语言中,编译器能够智能的分析出一个变量是否该分配在栈上还是堆上,分配在栈上的变量能够在函数声明周期结束之后立即被销毁吗,分配在堆上的在后期可以被GC过程销毁。由此可见,分配在栈上的变量是不会增加GC的负担的。

对于go来讲,如果一个变量的引用作为函数的返回值返回,那么将发生逃逸,因为它在函数返回后,仍然可能在其他的地方被其他的对象所引用。以下是可能发生逃逸的操作:

  1. 函数调用另外的函数。
  2. 引用被赋值给结构体成员,
  3. slice和map
  4. cgo对变量的指针引用。

可以通过在编译时加上-m -l参数来进行逃逸分析,其中-m将会输出逃逸分析相关信息,-l防止编译器进行自动的方法内联。

参考资料:

Golang中sync.Pool详解

我们通常用golang来构建高并发场景下的应用,但是由于golang内建的GC机制会影响应用的性能,为了减少GC,golang提供了对象重用的机制,也就是sync.Pool对象池。
sync.Pool是可伸缩的,并发安全的。其大小仅受限于内存的大小,可以被看作是一个存放可重用对象的值的容器。
设计的目的是存放已经分配的但是暂时不用的对象,在需要用到的时候直接从pool中取。

任何存放区其中的值可以在任何时候被删除而不通知,在高负载下可以动态的扩容,在不活跃时对象池会收缩。

sync.Pool首先声明了两个结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Local per-P Pool appendix.
type poolLocalInternal struct {
private interface{} // Can be used only by the respective P.
shared []interface{} // Can be used by any P.
Mutex // Protects shared.
}

type poolLocal struct {
poolLocalInternal

// Prevents false sharing on widespread platforms with
// 128 mod (cache line size) = 0 .
pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}

为了使得在多个goroutine中高效的使用goroutine,sync.Pool为每个P(对应CPU)都分配一个本地池,当执行Get或者Put操作的时候,会先将goroutine和某个P的子池关联,再对该子池进行操作。
每个P的子池分为私有对象和共享列表对象,私有对象只能被特定的P访问,共享列表对象可以被任何P访问。因为同一时刻一个P只能执行一个goroutine,所以无需加锁,但是对共享列表对象进行操作时,因为可能有多个goroutine同时操作,所以需要加锁。

值得注意的是poolLocal结构体中有个pad成员,目的是为了防止false sharing。cache使用中常见的一个问题是false sharing。当不同的线程同时读写同一cache line上不同数据时就可能发生false sharing。false sharing会导致多核处理器上严重的系统性能下降。具体的可以参考伪共享(False Sharing)

类型sync.Pool有两个公开的方法,一个是Get,一个是Put, 我们先来看一下Put的源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Put adds x to the pool.
func (p *Pool) Put(x interface{}) {
if x == nil {
return
}
if race.Enabled {
if fastrand()%4 == 0 {
// Randomly drop x on floor.
return
}
race.ReleaseMerge(poolRaceAddr(x))
race.Disable()
}
l := p.pin()
if l.private == nil {
l.private = x
x = nil
}
runtime_procUnpin()
if x != nil {
l.Lock()
l.shared = append(l.shared, x)
l.Unlock()
}
if race.Enabled {
race.Enable()
}
}
  1. 如果放入的值为空,直接return.
  2. 检查当前goroutine的是否设置对象池私有值,如果没有则将x赋值给其私有成员,并将x设置为nil。
  3. 如果当前goroutine私有值已经被设置,那么将该值追加到共享列表。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func (p *Pool) Get() interface{} {
if race.Enabled {
race.Disable()
}
l := p.pin()
x := l.private
l.private = nil
runtime_procUnpin()
if x == nil {
l.Lock()
last := len(l.shared) - 1
if last >= 0 {
x = l.shared[last]
l.shared = l.shared[:last]
}
l.Unlock()
if x == nil {
x = p.getSlow()
}
}
if race.Enabled {
race.Enable()
if x != nil {
race.Acquire(poolRaceAddr(x))
}
}
if x == nil && p.New != nil {
x = p.New()
}
return x
}
  1. 尝试从本地P对应的那个本地池中获取一个对象值, 并从本地池冲删除该值。
  2. 如果获取失败,那么从共享池中获取, 并从共享队列中删除该值。
  3. 如果获取失败,那么从其他P的共享池中偷一个过来,并删除共享池中的该值(p.getSlow())。
  4. 如果仍然失败,那么直接通过New()分配一个返回值,注意这个分配的值不会被放入池中。New()返回用户注册的New函数的值,如果用户未注册New,那么返回nil。

最后我们来看一下init函数。

1
2
3
func init() {
runtime_registerPoolCleanup(poolCleanup)
}

可以看到在init的时候注册了一个PoolCleanup函数,他会清除掉sync.Pool中的所有的缓存的对象,这个注册函数会在每次GC的时候运行,所以sync.Pool中的值只在两次GC中间的时段有效。

通过以上的解读,我们可以看到,Get方法并不会对获取到的对象值做任何的保证,因为放入本地池中的值有可能会在任何时候被删除,但是不通知调用者。放入共享池中的值有可能被其他的goroutine偷走。
所以对象池比较适合用来存储一些临时切状态无关的数据,但是不适合用来存储数据库连接的实例,因为存入对象池重的值有可能会在垃圾回收时被删除掉,这违反了数据库连接池建立的初衷。

参考资料:

Golang中channels详解

什么是channel

GO语言并发编程模型参考了了CSP理论,也就是所谓的通过传递消息来共享内容,而不是共享内存。你可以把channel想像成一根水管,发送者将消息从水管的一端发进去,接受者从水管的另一端取出来。
channel是有类型的,一端定义了channel的类型,那么发送者将只能发送特定类型的数据到该channel。
例如定义chan T,那么该chan将只能接受类型为T的数据。

发送和接收

channel的发送和读取的语法如下:

1
2
data := <- a //从channel中读取数据
a <- data //将数据发送到channel

需要注意的是,声明一个channel变量并不能直接使用,因为该变量的值是nil,无论是向nil的channel发送数据还是从nil的channel接收数据都将引发一个panic。

channel的读取和发送都是阻塞的,除非有另外的goroutine向channel的一端发送数据,或者从channel中读取数据。channel的这种特性能够帮助开发者不显示的使用锁来完成同步操作。

bufferd channel VS unbuffers channel

channel可以有两种初始化的方式,分别是初始化成带缓冲的和不带缓冲的。下面将介绍这两种方式的差异。

1
2
3
4
var a chan int
var b chan int
a = make(chan int) // 不带缓冲的chan
b = make(chan int, 3) //带缓冲的chan

对于带缓冲的channel,只要channel未满,我们可以一直往里面写数据,但是无缓冲的channel则不行,下面的一段程序可以验证上面的结论。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func main() {
a := make(chan int)
b := make(chan int, 3)
b <- 1
b <- 2
b <- 3
fmt.Println(<-b)
fmt.Println(<-b)
fmt.Println(<-b)
go func() {
fmt.Println(<-a)
}
a <- 1
}

channel with select

可以通过select模式来实现多个chan的收发操作,还可以定义一个timer来实现超时的逻辑处理。

1
2
3
4
5
6
7
8
9
10
11
12
select {
case x := <- a:
//do something
case y, ok := <- b:
//do something
case c <- z:
//do something
case <- time.After(....):
//do something
default:
//do something
}

for..range chan 和 close

channel也可以像slice一样通过for range来遍历,只要没关闭channel,for语句将一直阻塞。

1
2
3
4
for c, ok := range b {
....
//do something
}

上面代码中的ok可以用来标示channel是否已经关闭。我们偶尔也会见到下面的用法。

1
2
3
4
for range b {
....
//do something
}

这里只要b不关闭,并且有数据发送到b,就可以一直执行for loop中的逻辑。
当一个带缓冲的channel被关闭后,我们仍然可以接收已经发送到该channel的数据直到完全取出所有的数据 ,
取完所有的数据之后再进行读取,将会得到声明的channel的类型的零值。
关闭一个不带缓冲的channel后,也可以从该channel读取数据,读取的是声明的channel的类型的零值。
无论是带缓冲的还是不带缓冲的channel,一旦关闭之后,就不能再向其发送数据了。
我们可以通过:

1
2
3
if c, ok := <-b; ok {
//do something
}

来判断channel是否关闭。

directional channel VS undirectional channel

通常我们使用时不会声明一个带方向的chan,因为只能接收或者只能发送的channel并没有实际的用处。
带方向的chan一般是用在方法的签名中,更多的时候是做一个约定,在代码review的时候可以更容易的看出该chan是用来读取还是用来接收。

Golang中goroutine的管理

最近在研究nsq的源码,首先看了下官方的一些文档,看到了一些对于goroutine管理方面的技巧,这里记录一下,备查。

使用go关键字能够很容易的创建一个goroutine,但是对于goroutine的管理和清理却并不那么容易。goroutine带来的死锁问题也是不可回避的,这通常是由于顺序处理不当造成的。
goroutine也能造成内存泄漏,这在长时间运行的系统中会造成很大的影响。

sync.WaitGroup

waitGroup可以用来记录goroutine的数量,并且提供一种方式来等待所有的goroutine退出, nsq包装了waitGroup,方便后续的调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type WaitGroupWrapper struct {
sync.WaitGroup
}

func (w *WaitGroupWrapper) Wrap(cb func()) {
w.Add(1)
go func() {
cb()
w.Done()
}()
}

// can be used as follows:
wg := WaitGroupWrapper{}
wg.Wrap(func() { n.idPump() })
...
wg.Wait()

Exit Signal

一种最简单的方式就是给每个goroutine传递一个chan,当关闭这个chan的时候,所有的goroutine都能够收到通知,这样就能知道什么时候该退出了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func work() {
exitChan := make(chan int)
go task1(exitChan)
go task2(exitChan)
time.Sleep(5 * time.Second)
close(exitChan)
}
func task1(exitChan chan int) {
<-exitChan
log.Printf("task1 exiting")
}

func task2(exitChan chan int) {
<-exitChan
log.Printf("task2 exiting")
}

退出时的同步

实现一个可靠的,无死锁的,所有传递中的消息的退出顺序是相当困难的, 下面是一些TIPS:

  • 理想的情况是负责发送数据到 go-chan 的 goroutine 也应负责关闭它。
  • 如果 message 不能丢失,确保相关的 go-chan 被清空(尤其是无缓冲的!),以保证发送者可以继续发送数据。
  • 另外,如果消息是不重要的,发送给一个单一的 go-chan 应转换为一个 select 附加一个退出信号(如上所述),以避免阻塞。
  • 一般的顺序应该是

    • 停止接受新的连接
    • 发送退出信号给 child goroutines
    • 利用 WaitGroup 的 wait 等待 goroutine 退出
    • 恢复缓冲数据
    • 刷新所有东西到硬盘

Golang中类型系统详解

最近看到一篇关于指导go语言新手怎么学习的文章,里面讲到了named type, unnamed type,恰好这一块有点印象,但是记忆已经不是太清晰了,所以本文总结一下,加深理解和记忆。

1.Named Type VS Unnamed Type

golang提供了一些基础类型(pre-delared)如boolean, numeric, string。基础类型可以组合成组合类型,这些类型有数组,切片,指针,结构体,map以及channel。

通过关键字type定义的类型叫做named type,前文描述的组合类型叫做unamed type。值得提出的一点就是基础类型也是named type。

可以为named type添加方法集,但是为unamed type添加方法集将会造成编译不通过。新定义的类型不会继承named type或者unamed type的方法集。

2.底层类型

named type的底层类型是基础类型或者组合类型,基础类型和组合类型的底层类型是它们自己。

3.赋值

go是静态类型的,每个变量都有一个静态类型,在编译时,一个变量只有一种类型,不能被隐式的转换。
如果named type的底层类型是基础类型,那么通过named type声明的变量和基础类型声明的变量之间不可以赋值,即使这两者的底层类型是一致的。
unamed type和组合类型之间是可以相互赋值的。