分类 golang 下的文章

写在前面

Go中的错误处理与Java、JavaScript或Python等其他主流编程语言略有不同。Go的内置错误不包含堆栈跟踪,也不支持传统的尝试/捕获方法来处理它们。相反,Go中的错误只是函数返回的值,可以用与任何其他数据类型几乎相同的方式来处理它们,这导致了一个惊人的轻量级和简单的设计。

在本文中,我将演示Go中处理错误的基础知识,以及一些可以在代码中遵循的简单策略,以确保程序健壮且易于调试。

error关键字

在golang中error关键字是一个interface,他的定义如下:

type error interface {
    Error() string
}

因此任何实现了interface的类型,都可以有Error() 的表现,它会返回一段字符串,也正是这样简介的表达,让golang中的错误处理异与任何其他编程语言。

实现接口

在golang标准库中,总能让我看到特别优化的设计,就比如如何实现一个错误,官方标准库如下:

- 阅读剩余部分 -

http keep alive

写在开始:

在早期的HTTP/1.0中, 协议是一个建立连接, 交互数据, 断开连接的过程. 然后作为客户端, 我们并不是加载一个uri就完成任务, 客户端需要非常频繁的和同一个服务端交互数据, 这个时候就好比我们有很多很多的电话内容要沟通, 但是又需要一件事情一次电话. 这个能想的到的性能问题.
作为互联网如此重要的协议,几乎承担了互联网绝大多数的应用层流量,为了解决这个问题,HTTP/1.0后期使用请求头: Connection: keep-alive 来告诉对方我请求完毕请不要关闭连接,后面我还有继续复用这个连接, HTTP/1.1 版本则默认使用keep-alive特性(值得注意的是这里的keep-alive特性和tcp的keepalive是完全不同的概念, 解决的也不是同一类问题).

keep-alive特性如何开启

因为golang的client,server端实现均使用HTTP/1.1规范, 所以无论你是否传递Connection: keep-alive头, 均默认开启keep-alive特性.

服务端默认开启keep-alive的源码分析

这一点可以从golang源码中看到:

首先我们看作为服务端, 当启动一个server后, 首先listen, 然后发起server处理:

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}
func (srv *Server) ListenAndServe() error {
    if srv.shuttingDown() {
        return ErrServerClosed
    }
    addr := srv.Addr
    if addr == "" {
        addr = ":http"
    }
        // 这里创建tcp链接
    ln, err := net.Listen("tcp", addr)
    if err != nil {
        return err
    }
        // 这里开始接受tcp数据包, 进行处理
    return srv.Serve(ln)
}

之后进入for循环, 一个一个的请求都将启动新的协程去处理, 由此我们也可以发现, 每一个http的请求都是通过新的协程去处理

for {
        rw, err := l.Accept()
                // 此处省略N行代码
        c.setState(c.rwc, StateNew) // before Serve can return
        go c.serve(connCtx)
    }

再进入serve方法后, 可以发现整体就是for循环一直处理该请求链接上的数据, 同时我们可以发现中间有判断doKeepAlives而退出循环, 这也是作为服务端程序, 你如果不想启用http keepalive而可以设置的选项http.Server.SetKeepAlivesEnabled(false)

客户端默认开启keep-alive的源码分析

客户端的源码在net/http/client.go中, 追踪客户端的代码有个核心的思想就是拿req取resp, 在client每一次发起请求都是 client结构体的do方法

if resp, didTimeout, err = c.send(req, deadline); err != nil {

往下追踪后知道client的所有req得到resp都是从 resp, err = rt.RoundTrip(req)发起,这也是设计者设计RoundTripper接口的目的.

最后queueForIdleConn方法可以得知, 我们的客户端在当前client结构体实例上是预先申请好连接的, 所以也就得到了keep-alive的目的.

// queueForIdleConn queues w to receive the next idle connection for w.cm.
// As an optimization hint to the caller, queueForIdleConn reports whether
// it successfully delivered an already-idle connection.
func (t *Transport) queueForIdleConn(w *wantConn) (delivered bool) {
    if t.DisableKeepAlives {
        return false
    }

    t.idleMu.Lock()
    defer t.idleMu.Unlock()

这是idleConn的直接体现, 同时, 方法一开始就判断了, 如果Transport结构体的实例如果设置了DisableKeepAlives的值为非0值, 则关闭连接池.

关于客户端连接池设计中的几个重要的参数

DisableKeepAlives

这个在上文中已经说过, 就是客户端Transport实例明确告诉服务端, 是否要开启长连接.

MaxConnsPerHost

代表每个客户端可以在一个host上创建最多几个连接

想了解 httpclient的连接池, 必须去阅读getConn方法, 如下有代码注释

// 这里获取连接池中的空闲链接
if delivered := t.queueForIdleConn(w); delivered {
    pc := w.pc
    // Trace only for HTTP/1.
    // HTTP/2 calls trace.GotConn itself.
    if pc.alt == nil && trace != nil && trace.GotConn != nil {
        trace.GotConn(pc.gotIdleConnTrace(pc.idleAt))
    }
    // set request canceler to some non-nil function so we
    // can detect whether it was cleared between now and when
    // we enter roundTrip
    t.setReqCanceler(treq.cancelKey, func(error) {})
    return pc, nil
}

cancelc := make(chan error, 1)
t.setReqCanceler(treq.cancelKey, func(err error) { cancelc <- err })

// 如果当前连接池中无空闲链接, 则开始异步创建连接
t.queueForDial(w)

进入 创建连接代码

// queueForDial queues w to wait for permission to begin dialing.
// Once w receives permission to dial, it will do so in a separate goroutine.
func (t *Transport) queueForDial(w *wantConn) {
    w.beforeDial()
        // 如果MaxConnsPerHost不够, 创建
    if t.MaxConnsPerHost <= 0 {
        go t.dialConnFor(w)
        return
    }

    t.connsPerHostMu.Lock()
    defer t.connsPerHostMu.Unlock()

    if n := t.connsPerHost[w.key]; n < t.MaxConnsPerHost {
        if t.connsPerHost == nil {
            t.connsPerHost = make(map[connectMethodKey]int)
        }
        t.connsPerHost[w.key] = n + 1
        go t.dialConnFor(w)
        return
    }

    if t.connsPerHostWait == nil {
        t.connsPerHostWait = make(map[connectMethodKey]wantConnQueue)
    }
    q := t.connsPerHostWait[w.key]
    q.cleanFront()
    q.pushBack(w)
    t.connsPerHostWait[w.key] = q
}

总结:

http 作为开发者是最最常用的类库, 其中有一些设计细节是有异于其他http类库的, 我们更需要keep-avlie的设计细节, 关注连接池的使用, 来达到更高的吞吐量, 这些地方优化好, 是可以整体提高我们程序的性能.

love

前述

Go1.14已经发布有一阵子了,下面是我学习的过程,也是知识的整理.

下载地址

下载地址

详细说明

可查看本地安装之后的doc文件夹,里面包含了go1.14.html文件,直接在本地打开即可食用

哪些地方做了优化

工具层面

go module

1.Flags
-modfile=file,现在可以直接指定mod file了,不在是固定的项目目录下了

语言&&Runtime

聚合接口

在1.14之前,我们聚合接口的定义不能有相同签名和返回值的方法,但是1.14取消了这个限制.

type Foo interface {
    GetName() string
    SetFooName(string)
}

type Bar interface {
    GetName() string
    SetBarName() string
}

type FooBar interface {
    Foo
    Bar
}
defer零消耗

1.14之前,defer的runtime要维护堆栈,我们要压栈和出栈的性能损耗,但是现在在编译器层做了defer的优化

goroutine 基于信号的抢占式调度实现

经典的例子,设置p为1,这样规避并发,然后go协程初始化到队列等待runtime调度,然而for循环的存在,调度一直调度不到,然后现在1.14可以利用信号机制抢占for的循环资源了.

package main

import (
    "runtime"
)

func main() {
    runtime.GOMAXPROCS(1)

    go func() {
        panic("already call")
    }()

    for {
    }
}

思考的问题:抢占是否影响性能 ?

抢占通常是由于阻塞性系统调用引起的,例如磁盘io,cgo等,所以一定程度上很难形成协程阻塞调度.

计时器优化,内存分配器

这里主要学习 这篇文章

包层面

test包
func TestIt(t *testing.T) {
    t.Cleanup(func() {
        fmt.Println("clean up 一些事情")
    })
    
    t.Log("这是个log")
    time.Sleep(time.Second * 2)
}

go1.14 会在sleep之前就输出log日志.同时增加了Cleanup方法(这个方法很有用,在我们测试完一个用例后可以做一些善后处理,例如rollback db等)

hash/maphash

降低了hash冲突的可能性.

json

InputOffset()方法

func main() {
    const jsonStr = `
        {"Job":"java", "Name":"zhangsan"}
        {"Job":"php", "Name":"lisi"},
        {"Job":"python", "Name":"wangwu"}
`
    type Jober struct {
        Job, Name string
    }

    dec := json.NewDecoder(strings.NewReader(jsonStr))
    for {
        var m Jober
        if err := dec.Decode(&m); err == io.EOF {
            break
        } else if err != nil {
            log.Fatal(err)
        }
        fmt.Println(m)
        fmt.Println(dec.InputOffset())
    }

}

输出:

{java zhangsan}
36
{php lisi}
67
2020/03/11 21:39:57 invalid character ',' looking for beginning of value

这里我特意后面多加了',', 可以看的出来解析的时候加入InputOffset很有用

ioutil

TempDir 第二个参数,随机字符串模式,可以是一个更加定制化的模式

    ioutil.TempDir("/tmp/ss", "name-date-*-test")

这里的 * 可以在一个字符串模式的中间

log
logger = log.New(&buf, "logger", log.Lmsgprefix)

文件名和行数放在prefix的前面

reflect包

StructOf 可以设置私有字段了

func main() {
    typ := reflect.StructOf([]reflect.StructField{
        {
            Name: "Height",
            Type: reflect.TypeOf(float64(0)),
            Tag:  `json:"height"`,
        },
        {
            Name: "age",
            Type: reflect.TypeOf(int(0)),
            Tag:  `json:"age"`,
            PkgPath: "other/path",
        },
    })

    v := reflect.New(typ).Elem()
    v.Field(0).SetFloat(0.4)
    v.Field(1).SetInt(2)
    s := v.Addr().Interface()

    w := new(bytes.Buffer)
    if err := json.NewEncoder(w).Encode(s); err != nil {
        panic(err)
    }

    fmt.Printf("value: %+v\n", s)
    fmt.Printf("json:  %s", w.Bytes())

    r := bytes.NewReader([]byte(`{"height":1.5,"age":10}`))
    if err := json.NewDecoder(r).Decode(s); err != nil {
        panic(err)
    }
    fmt.Printf("value: %+v\n", s)

}

这里的age是一个不可导出的私有属性,但是我同样可以在structof中设置

runtime

Goexit()

func todo() {
    defer func() {
        fmt.Println(recover())
    }()
    defer panic("cancelled goexit")
    runtime.Goexit()
}

func main() {
    todo()
}

这里在1.13中,由于goexit退出当前协程而报错,但是此时会被recover住,但是1.14将会顺利报错.

strconv包

strconv包中的NumError类型现在支持is判断不同的类型,代码如下:

    str := "hello world"
    if _, err := strconv.ParseFloat(str, 64); err != nil {
        if errors.Is(err, strconv.ErrSyntax) {

        }
        if errors.Is(err, strconv.ErrRange) {

        }
    }

前述

我们在写一个web模块或者api接口的时候,我们总是能够想到利用mvc模式,将model层,controller的action层分离, 使得我们的代码总是干净,可维护性强,注入新的服务简单.这一篇博客,我将探索在go中如何利用golang语言的特性,写一个干净,整洁,注入型强的代码.

一个简单的代码需求

  1. 起一个web server
  2. 实现用户注册接口
  3. 单元测试完备
  4. 后期新的需求,可以快速在现有代码的基础上完成

尽量编写testable的代码

我们总是希望,我们的testing中枚举用例不受过多服务的牵连,也不受过多业务逻辑的牵连
例如,用户有existscreate方法,那我们最好的方式则是创建一组这样的user相关的接口,大致代码如下:

type Repository interface {
    Exists(email string) bool
    Create(*Form) (*User, error)
}
func (m *Memstore) Exists(email string) bool {
    return true
}

func (m *Memstore) Create(*Form) (*User, error) {
    return &User{
        Id:       1,
        Email:    "test@qq.com",
        Password: "123",
    }, nil
}

还有我们的user

type User struct {
    Id int `json:"id"`
    Email string `json:"email"`
    Password string `json:"password"`
}

那么我们对服务的testing可以这样:

func TestMemstore_Exists(t *testing.T) {
    type fields struct {
        Users []User
    }
    type args struct {
        email string
    }
    tests := []struct {
        name   string
        fields fields
        args   args
        want   bool
    }{
        // TODO: Add test cases.
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            m := &Memstore{
                Users: tt.fields.Users,
            }
            if got := m.Exists(tt.args.email); got != tt.want {
                t.Errorf("Exists() = %v, want %v", got, tt.want)
            }
        })
    }
}

实现handler

我们绑定/register 接口到handle上面, 注意这里不是handleFunc, 我们需要做的是在handle上注入更多的toolings,例如: model模型,Validater组件等

// 他需要两个服务,一个create,一个exists
type RegisterHandler struct {
    Repository
}

// 实现注册服务
func (r *RegisterHandler) ServeHTTP(http.ResponseWriter, *http.Request) {

}

这里有很重要的两点, 我们自己实现了http包的handler interface,我们将可testing的model模型绑定到了registerhandler上

我们在这里还可以绑定更多的组件

type RegistrationHandler struct {
    Validator *validator.Validate
    Repository

实现handler接口的绑定

func NewServer(addr string, r Repository) error {
    mux := http.NewServeMux()
    h := &RegisterHandler{r}
    mux.Handle("register", h)

    server := http.Server{
        Addr:    addr,
        Handler: h,
    }
    err := server.ListenAndServe()
    if err != nil {
        return err
    }
    return nil
}

这里将h注册到handle上,并将Repository具体实现传递给handler.

总结

当新的需求来临时,代码耦合的可能性增加,因为没有明显的设计来保护。服务对象的方式我们传递给每一个handler,使得每一个对象自己的独立功能尽可能不耦合,这样后期的可维护性也就大大增加了.

需求场景

我需要对http请求的body体做flter处理,以及拿到request的body做验签,之后http的request body又会参与二次或者多次的使用,例如透明传递给backend做转发,流量拷贝等.

名词解释

backend: 这个backend是我需要将流量(http 请求)复制给后端server,然后拿到后端server的结果返回给前端,整个过程你可以理解为是gateway或者类似nginx的porxy
所以这里的backend指的是: 后端server或者后端server的一些特征属性组成的一个实例.

bug复现

前端同学反馈给我,说backend提供了一个post请求的接口,直接访问backend的接口是200的status,且没有respose body,但是在网关层访问返回500.

查找bug思路第一步:

第一步,赶紧找log,所以log内容如下:

net/http: HTTP/1.x transport connection broken: http: ContentLength=30 with Body length 0

从日志看来是body的内容指定了content-length,但是是空body对应一个非0值的content-length

查找bug思路第二步->误入歧途:

这里我的第一脑补是backend的结果是空导致的,是不是backend返回给我了一个空body的情况下content-length是非0?
所以我就开始直接构造请求到backend
结果是:backend 返回没有问题, 到此我开始了阅读roundtrip源代码的漫长之路,可是整个过程很复杂,涉及到异步,涉及到http 协议的底层.

查找bug思路的第三步->回归现象:

无奈,源码阅读之后并无头绪,所以重新回归现象,仔细分析

其实log中可以看出来,说明以及很明确了,一定是body内容和content-length头不一致导致的,所以这里反思之后觉得是不是请求body的问题?

一旦想到这一点,我便去构造不同的body来验证length长度,果然length长度随着请求的body不同而不同

查找bug思路第四步->初见端倪:

一定是body被某一层的代码读走了,而roundtrip依然要使用,所以开始了各个flter层的查看,最后找到罪魁祸首:

bodyString, _ := ctx.GetRawData()

果然body被拿走,而没有回写,导致roundtrip拿不到body了

解决bug:

找到原因后,解bug不足一分钟
思路:回写request body到ctx中

bodyString, _ := ctx.GetRawData()
ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer([]byte(bodyString)))

总结:

当你读io的时候。读取时,它会把它读取完。一旦你阅读它,内容就消失了。你不能再读一遍。
io.reader 接近与水龙头:你可以得到水,但一旦它出来了,它就出来了。