标签 goroutine 下的文章

Go标准库http与fasthttp服务端性能比较

本文永久链接 – https://tonybai.com/2021/04/25/server-side-performance-nethttp-vs-fasthttp

1. 背景

Go初学者学习Go时,在编写了经典的“hello, world”程序之后,可能会迫不及待的体验一下Go强大的标准库,比如:用几行代码写一个像下面示例这样拥有完整功能的web server:

// 来自https://tip.golang.org/pkg/net/http/#example_ListenAndServe
package main

import (
    "io"
    "log"
    "net/http"
)

func main() {
    helloHandler := func(w http.ResponseWriter, req *http.Request) {
        io.WriteString(w, "Hello, world!\n")
    }
    http.HandleFunc("/hello", helloHandler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

go net/http包是一个比较均衡的通用实现,能满足大多数gopher 90%以上场景的需要,并且具有如下优点:

  • 标准库包,无需引入任何第三方依赖;
  • 对http规范的满足度较好;
  • 无需做任何优化,即可获得相对较高的性能;
  • 支持HTTP代理;
  • 支持HTTPS;
  • 无缝支持HTTP/2。

不过也正是因为http包的“均衡”通用实现,在一些对性能要求严格的领域,net/http的性能可能无法胜任,也没有太多的调优空间。这时我们会将眼光转移到其他第三方的http服务端框架实现上。

而在第三方http服务端框架中,一个“行如其名”的框架fasthttp被提及和采纳的较多,fasthttp官网宣称其性能是net/http的十倍(基于go test benchmark的测试结果)。

fasthttp采用了许多性能优化上的最佳实践,尤其是在内存对象的重用上,大量使用sync.Pool以降低对Go GC的压力。

那么在真实环境中,到底fasthttp能比net/http快多少呢?恰好手里有两台性能还不错的服务器可用,在本文中我们就在这个真实环境下看看他们的实际性能。

2. 性能测试

我们分别用net/http和fasthttp实现两个几乎“零业务”的被测程序:

  • nethttp:
// github.com/bigwhite/experiments/blob/master/http-benchmark/nethttp/main.go
package main

import (
    _ "expvar"
    "log"
    "net/http"
    _ "net/http/pprof"
    "runtime"
    "time"
)

func main() {
    go func() {
        for {
            log.Println("当前routine数量:", runtime.NumGoroutine())
            time.Sleep(time.Second)
        }
    }()

    http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, Go!"))
    }))

    log.Fatal(http.ListenAndServe(":8080", nil))
}
  • fasthttp:
// github.com/bigwhite/experiments/blob/master/http-benchmark/fasthttp/main.go

package main

import (
    "fmt"
    "log"
    "net/http"
    "runtime"
    "time"

    _ "expvar"

    _ "net/http/pprof"

    "github.com/valyala/fasthttp"
)

type HelloGoHandler struct {
}

func fastHTTPHandler(ctx *fasthttp.RequestCtx) {
    fmt.Fprintln(ctx, "Hello, Go!")
}

func main() {
    go func() {
        http.ListenAndServe(":6060", nil)
    }()

    go func() {
        for {
            log.Println("当前routine数量:", runtime.NumGoroutine())
            time.Sleep(time.Second)
        }
    }()

    s := &fasthttp.Server{
        Handler: fastHTTPHandler,
    }
    s.ListenAndServe(":8081")
}

对被测目标实施压力测试的客户端,我们基于hey这个http压测工具进行,为了方便调整压力水平,我们将hey“包裹”在下面这个shell脚本中(仅适于在linux上运行):

// github.com/bigwhite/experiments/blob/master/http-benchmark/client/http_client_load.sh

# ./http_client_load.sh 3 10000 10 GET http://10.10.195.181:8080
echo "$0 task_num count_per_hey conn_per_hey method url"
task_num=$1
count_per_hey=$2
conn_per_hey=$3
method=$4
url=$5

start=$(date +%s%N)
for((i=1; i<=$task_num; i++)); do {
    tm=$(date +%T.%N)
        echo "$tm: task $i start"
    hey -n $count_per_hey -c $conn_per_hey -m $method $url > hey_$i.log
    tm=$(date +%T.%N)
        echo "$tm: task $i done"
} & done
wait
end=$(date +%s%N)

count=$(( $task_num * $count_per_hey ))
runtime_ns=$(( $end - $start ))
runtime=`echo "scale=2; $runtime_ns / 1000000000" | bc`
echo "runtime: "$runtime
speed=`echo "scale=2; $count / $runtime" | bc`
echo "speed: "$speed

该脚本的执行示例如下:

bash http_client_load.sh 8 1000000 200 GET http://10.10.195.134:8080
http_client_load.sh task_num count_per_hey conn_per_hey method url
16:58:09.146948690: task 1 start
16:58:09.147235080: task 2 start
16:58:09.147290430: task 3 start
16:58:09.147740230: task 4 start
16:58:09.147896010: task 5 start
16:58:09.148314900: task 6 start
16:58:09.148446030: task 7 start
16:58:09.148930840: task 8 start
16:58:45.001080740: task 3 done
16:58:45.241903500: task 8 done
16:58:45.261501940: task 1 done
16:58:50.032383770: task 4 done
16:58:50.985076450: task 7 done
16:58:51.269099430: task 5 done
16:58:52.008164010: task 6 done
16:58:52.166402430: task 2 done
runtime: 43.02
speed: 185960.01

从传入的参数来看,该脚本并行启动了8个task(一个task启动一个hey),每个task向http://10.10.195.134:8080建立200个并发连接,并发送100w http GET请求。

我们使用两台服务器分别放置被测目标程序和压力工具脚本:

  • 目标程序所在服务器:10.10.195.181(物理机,Intel x86-64 CPU,40核,128G内存, CentOs 7.6)
$ cat /etc/redhat-release
CentOS Linux release 7.6.1810 (Core) 

$ lscpu
Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                40
On-line CPU(s) list:   0-39
Thread(s) per core:    2
Core(s) per socket:    10
座:                 2
NUMA 节点:         2
厂商 ID:           GenuineIntel
CPU 系列:          6
型号:              85
型号名称:        Intel(R) Xeon(R) Silver 4114 CPU @ 2.20GHz
步进:              4
CPU MHz:             800.000
CPU max MHz:           2201.0000
CPU min MHz:           800.0000
BogoMIPS:            4400.00
虚拟化:           VT-x
L1d 缓存:          32K
L1i 缓存:          32K
L2 缓存:           1024K
L3 缓存:           14080K
NUMA 节点0 CPU:    0-9,20-29
NUMA 节点1 CPU:    10-19,30-39
Flags:                 fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch epb cat_l3 cdp_l3 intel_pt ssbd mba ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm cqm mpx rdt_a avx512f avx512dq rdseed adx smap clflushopt clwb avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 cqm_llc cqm_occup_llc cqm_mbm_total cqm_mbm_local dtherm ida arat pln pts pku ospke spec_ctrl intel_stibp flush_l1d

  • 压力工具所在服务器:10.10.195.133(物理机,鲲鹏arm64 cpu,96核,80G内存, CentOs 7.9)
# cat /etc/redhat-release
CentOS Linux release 7.9.2009 (AltArch)

# lscpu
Architecture:          aarch64
Byte Order:            Little Endian
CPU(s):                96
On-line CPU(s) list:   0-95
Thread(s) per core:    1
Core(s) per socket:    48
座:                 2
NUMA 节点:         4
型号:              0
CPU max MHz:           2600.0000
CPU min MHz:           200.0000
BogoMIPS:            200.00
L1d 缓存:          64K
L1i 缓存:          64K
L2 缓存:           512K
L3 缓存:           49152K
NUMA 节点0 CPU:    0-23
NUMA 节点1 CPU:    24-47
NUMA 节点2 CPU:    48-71
NUMA 节点3 CPU:    72-95
Flags:                 fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma dcpop asimddp asimdfhm

我用dstat监控被测目标所在主机资源占用情况(dstat -tcdngym),尤其是cpu负荷;通过expvarmon监控memstats,由于没有业务,内存占用很少;通过go tool pprof查看目标程序中对各类资源消耗情况的排名。

下面是多次测试后制作的一个数据表格:


图:测试数据

3. 对结果的简要分析

受特定场景、测试工具及脚本精确性以及压力测试环境的影响,上面的测试结果有一定局限,但却真实反映了被测目标的性能趋势。我们看到在给予同样压力的情况下,fasthttp并没有10倍于net http的性能,甚至在这样一个特定的场景下,两倍于net/http的性能都没有达到:我们看到在目标主机cpu资源消耗接近70%的几个用例中,fasthttp的性能仅比net/http高出30%~70%左右。

那么为什么fasthttp的性能未及预期呢?要回答这个问题,那就要看看net/http和fasthttp各自的实现原理了!我们先来看看net/http的工作原理示意图:


图:nethttp工作原理示意图

http包作为server端的原理很简单,那就是accept到一个连接(conn)之后,将这个conn甩给一个worker goroutine去处理,后者一直存在,直到该conn的生命周期结束:即连接关闭。

下面是fasthttp的工作原理示意图:


图:fasthttp工作原理示意图

而fasthttp设计了一套机制,目的是尽量复用goroutine,而不是每次都创建新的goroutine。fasthttp的Server accept一个conn之后,会尝试从workerpool中的ready切片中取出一个channel,该channel与某个worker goroutine一一对应。一旦取出channel,就会将accept到的conn写到该channel里,而channel另一端的worker goroutine就会处理该conn上的数据读写。当处理完该conn后,该worker goroutine不会退出,而是会将自己对应的那个channel重新放回workerpool中的ready切片中,等待这下一次被取出

fasthttp的goroutine复用策略初衷很好,但在这里的测试场景下效果不明显,从测试结果便可看得出来,在相同的客户端并发和压力下,net/http使用的goroutine数量与fasthttp相差无几。这是由测试模型导致的:在我们这个测试中,每个task中的hey都会向被测目标发起固定数量的长连接(keep-alive),然后在每条连接上发起“饱和”请求。这样fasthttp workerpool中的goroutine一旦接收到某个conn就只能在该conn上的通讯结束后才能重新放回,而该conn直到测试结束才会close,因此这样的场景相当于让fasthttp“退化”成了net/http的模型,也染上了net/http的“缺陷”:goroutine的数量一旦多起来,go runtime自身调度所带来的消耗便不可忽视甚至超过了业务处理所消耗的资源占比。下面分别是fasthttp在200长连接、8000长连接以及16000长连接下的cpu profile的结果:

200长连接:

(pprof) top -cum
Showing nodes accounting for 88.17s, 55.35% of 159.30s total
Dropped 150 nodes (cum <= 0.80s)
Showing top 10 nodes out of 60
      flat  flat%   sum%        cum   cum%
     0.46s  0.29%  0.29%    101.46s 63.69%  github.com/valyala/fasthttp.(*Server).serveConn
         0     0%  0.29%    101.46s 63.69%  github.com/valyala/fasthttp.(*workerPool).getCh.func1
         0     0%  0.29%    101.46s 63.69%  github.com/valyala/fasthttp.(*workerPool).workerFunc
     0.04s 0.025%  0.31%     89.46s 56.16%  internal/poll.ignoringEINTRIO (inline)
    87.38s 54.85% 55.17%     89.27s 56.04%  syscall.Syscall
     0.12s 0.075% 55.24%     60.39s 37.91%  bufio.(*Writer).Flush
         0     0% 55.24%     60.22s 37.80%  net.(*conn).Write
     0.08s  0.05% 55.29%     60.21s 37.80%  net.(*netFD).Write
     0.09s 0.056% 55.35%     60.12s 37.74%  internal/poll.(*FD).Write
         0     0% 55.35%     59.86s 37.58%  syscall.Write (inline)
(pprof) 

8000长连接:

(pprof) top -cum
Showing nodes accounting for 108.51s, 54.46% of 199.23s total
Dropped 204 nodes (cum <= 1s)
Showing top 10 nodes out of 66
      flat  flat%   sum%        cum   cum%
         0     0%     0%    119.11s 59.79%  github.com/valyala/fasthttp.(*workerPool).getCh.func1
         0     0%     0%    119.11s 59.79%  github.com/valyala/fasthttp.(*workerPool).workerFunc
     0.69s  0.35%  0.35%    119.05s 59.76%  github.com/valyala/fasthttp.(*Server).serveConn
     0.04s  0.02%  0.37%    104.22s 52.31%  internal/poll.ignoringEINTRIO (inline)
   101.58s 50.99% 51.35%    103.95s 52.18%  syscall.Syscall
     0.10s  0.05% 51.40%     79.95s 40.13%  runtime.mcall
     0.06s  0.03% 51.43%     79.85s 40.08%  runtime.park_m
     0.23s  0.12% 51.55%     79.30s 39.80%  runtime.schedule
     5.67s  2.85% 54.39%     77.47s 38.88%  runtime.findrunnable
     0.14s  0.07% 54.46%     68.96s 34.61%  bufio.(*Writer).Flush

16000长连接:

(pprof) top -cum
Showing nodes accounting for 239.60s, 87.07% of 275.17s total
Dropped 190 nodes (cum <= 1.38s)
Showing top 10 nodes out of 46
      flat  flat%   sum%        cum   cum%
     0.04s 0.015% 0.015%    153.38s 55.74%  runtime.mcall
     0.01s 0.0036% 0.018%    153.34s 55.73%  runtime.park_m
     0.12s 0.044% 0.062%       153s 55.60%  runtime.schedule
     0.66s  0.24%   0.3%    152.66s 55.48%  runtime.findrunnable
     0.15s 0.055%  0.36%    127.53s 46.35%  runtime.netpoll
   127.04s 46.17% 46.52%    127.04s 46.17%  runtime.epollwait
         0     0% 46.52%       121s 43.97%  github.com/valyala/fasthttp.(*workerPool).getCh.func1
         0     0% 46.52%       121s 43.97%  github.com/valyala/fasthttp.(*workerPool).workerFunc
     0.41s  0.15% 46.67%    120.18s 43.67%  github.com/valyala/fasthttp.(*Server).serveConn
   111.17s 40.40% 87.07%    111.99s 40.70%  syscall.Syscall
(pprof)

通过上述profile的比对,我们发现当长连接数量增多时(即workerpool中goroutine数量增多时),go runtime调度的占比会逐渐提升,在16000连接时,runtime调度的各个函数已经排名前4了。

4. 优化途径

从上面的测试结果,我们看到fasthttp的模型不太适合这种连接连上后进行持续“饱和”请求的场景,更适合短连接或长连接但没有持续饱和请求,在后面这样的场景下,它的goroutine复用模型才能更好的得以发挥。

但即便“退化”为了net/http模型,fasthttp的性能依然要比net/http略好,这是为什么呢?这些性能提升主要是fasthttp在内存分配层面的优化trick的结果,比如大量使用sync.Pool,比如避免在[]byte和string互转等。

那么,在持续“饱和”请求的场景下,如何让fasthttp workerpool中goroutine的数量不会因conn的增多而线性增长呢?fasthttp官方没有给出答案,但一条可以考虑的路径是使用os的多路复用(linux上的实现为epoll),即go runtime netpoll使用的那套机制。在多路复用的机制下,这样可以让每个workerpool中的goroutine处理同时处理多个连接,这样我们可以根据业务规模选择workerpool池的大小,而不是像目前这样几乎是任意增长goroutine的数量。当然,在用户层面引入epoll也可能会带来系统调用占比的增多以及响应延迟增大等问题。至于该路径是否可行,还是要看具体实现和测试结果。

注:fasthttp.Server中的Concurrency可以用来限制workerpool中并发处理的goroutine的个数,但由于每个goroutine只处理一个连接,当Concurrency设置过小时,后续的连接可能就会被fasthttp拒绝服务。因此fasthttp的默认Concurrency为:

const DefaultConcurrency = 256 * 1024

本文涉及的源码可以在这里 github.com/bigwhite/experiments/blob/master/http-benchmark 下载。


“Gopher部落”知识星球正式转正(从试运营星球变成了正式星球)!“gopher部落”旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!部落目前虽小,但持续力很强。在2021年上半年,部落将策划两个专题系列分享,并且是部落独享哦:

  • Go技术书籍的书摘和读书体会系列
  • Go与eBPF系列

欢迎大家加入!

Go技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中!本专栏主要满足广大gopher关于Go语言进阶的需求,围绕如何写出地道且高质量Go代码给出50条有效实践建议,上线后收到一致好评!欢迎大家订
阅!

img{512x368}

我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网热卖中,欢迎小伙伴们订阅学习!

img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 微信公众号:iamtonybai
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • “Gopher部落”知识星球:https://public.zsxq.com/groups/51284458844544

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

Go语言“十诫”[译]

本文永久链接 – https://tonybai.com/2021/04/09/ten-commandments-of-go

本文翻译自John Arundel的《Ten commandments of Go》。全文如下:

作为一名全职的Go语言作家老师,我花了很多时间和学生们一起,帮助他们写出更清晰、更好、更有用的Go程序。我发现,我给他们的建议可以归纳总结为一套通用原则,在这里我将这些原则分享给大家。

1. 你应该是无聊的

Go社区喜欢共识(consensus)。比如:Go源代码有一个由gofmt强制执行的统一的代码格式规范。同样,无论你要解决什么问题,通常都有一个标准的、类似于Go行事风格的方法来解决。有时它是标准的方式,因为它是最好的方式,但通常它只是最好的方式,因为它是标准的方式

要抵制住创意、时尚或(最糟糕的是)聪明的诱惑,这些不是Go的行事风格。Go行事风格的代码简单、无聊,通常相当啰嗦,而且最重要的是显式的风格(由于这个原因,有些人把Go称为面向显式(obviousness-oriented)风格的编程语言)。

当有疑问时,请遵循最小惊喜原则。争取做到一目了然。要直截了当,要简单,要显式,要无聊。

这并不是说在软件工程层面没有展示令人叹为观止的优雅和风格的空间了;当然有。但那是在设计层面上,而不是单个代码行。代码并不重要,它应该以被随时替换。重要的是程序。

2. 你应该以测试为先

在Go中,一个常见的错误是先写了一些函数(比如:GetDataFromAPI),然后在考虑如何测试它时不知所措。函数通过网络进行了真正的API调用,它向终端打印东西,它写磁盘文件了,这是一个可怕的的不可测试性的坑。

不要先写那个函数,而是先写一个测试(比如:TestGetDataFromAPI)。如何写这样一个测试呢?它必须为函数的调用提供一个本地的TLS测试服务器,所以你需要一种方法来注入这种依赖。它要写数据到io.Writer,你同样需要为此注入一个模拟外部世界的本地依赖,比如:bytes.Buffer。

现在,当你开始编写GetDataFromAPI函数时,一切都将变得很容易了。它的所有依赖关系都被注入,所以它的业务逻辑与它与外部世界的交互和监听方式完全脱钩。

HTTP handler也是如此。一个HTTP handler的唯一工作是解析请求中的数据,将其传递给某个业务逻辑函数来计算结果,并将结果格式化到ResponseWriter。这几乎不需要测试,所以你的大部分测试将在业务逻辑函数本身,而不是handler。我们知道HTTP的工作原理。

3. 你应该测试行为,而不是函数

如果你想知道如何在不实际调用API的情况下测试这个函数,那么答案很简单:”不要测试这个函数”。

你需要测试的不是一些函数,而是一些行为。例如,一个可能是”给定一些用户输入,我可以正确地组合URL并以正确的参数调用API。” 另一个可能是”给定API返回的一些JSON数据,我可以正确地将其解包到某个Go结构体中。”

当你沿着这样的思路考量问题的解决方法的时候,写测试就容易多了:你可以想象一些这类函数,它们每个函数都会接受一些输入,并产生一些输出,并且很容易给它们编写单元测试。有些事情它们是不会做的,例如进行任何HTTP调用。

同样,当你试图实现”数据可以持久地存储在数据库中并从数据库中检索”这样的行为时,你可以将其分解成更小的、更可测试的行为。例如,”给定一个Go结构体,我可以正确地生成SQL查询,并将其内容存储到Postgres表中”,或者 “给定一个对象,我可以正确地将结果解析到Go结构体切片中”。不需要mock数据库,不需要真正的数据库!

4. 你不应制造文书工作

所有的程序都会在某一点上涉及到一些繁琐的、不可避免的数据倒换重组活动;我们可以把所有这类活动归入文书工作的范畴。对程序员来说,唯一的问题是,这些文书工作在API边界的哪一边?

如果是放在用户侧,那就意味着用户必须编写大量的代码来为你的库准备文书工作,然后再编写大量的代码来将结果解压成有用的格式。

相反(将文书工作放在API实现侧),写零文书工作的库,可以在一行中调用:

game.Run()

不要让用户调用一个构造函数来获取某个对象,然后再基于这个对象进行方法调用。那就是文书工作。只要让一切在他们直接调用时发生就可以了。如果有可配置的设置,请设置合理的默认值,这样用户根本不用考虑,除非他们因为某些原因需要覆盖默认值。功能选项(functional option)是一个很好的模式。

这是另一个先写测试的好理由,如果你写的API中创造了文书工作,那么在测试时你将不得不自己做所有的文书工作,以便使用你自己的库。如果这被证明是笨拙、啰嗦和耗时的,可以考虑将这些文书工作移到API边界内。

5. 你不应该杀死程序

你的库没有权利终止用户的程序。不要在你的包中调用像os.Exit、log.Fatal、panic这样的函数,这不是你能决定的。相反,如果你遇到了不可恢复(recover)的错误,将它们返回给调用者。

为什么不呢?因为它迫使任何想使用你的库的人去写代码,不管panic是否真的被触发。出于同样的原因,你永远不应该使用会引起panic的第三方库,因为一旦你用了,你就需要recover它们。

所以你千万不要显式调用(这些可以杀死程序的函数),但是隐式调用呢?你所做的任何操作,在某些情况下可能会panic(比如:索引一个空的片断,写入一个空map,类型断言失败)都应该先检查一下是否正常,如果不正常就返回一个错误。

6. 你不要泄露资源

对于一个打算永远运行而不崩溃或出错的程序来说,对其的要求要比对单次命令行工具要严格一些。例如,想想太空探测器:在关键时刻意外重启制导系统,可能会让价值数十亿美元的飞行器驶向星系间的虚空。对于负责的软件工程师来说,这很可能会导致一场没有咖啡的面谈,让人有些不舒服。

我们不是都在为太空器写软件,但我们应该像太空工程师一样思考。自然,我们的程序应该永远不会崩溃(最坏的情况下,它们应该优雅地退化,并提出退出过程的详实信息),但它们也需要是可持续的。这意味着不能泄露内存、goroutines、文件句柄或任何其他稀缺资源。

每当你有一些可泄漏的资源时,当你知道你已经成功获得它的那一刻,你应该想着释放它。无论函数如何退出或何时退出,保证将其清理掉,我们可以用Go带给我们的礼物:defer

任何时候启动一个goroutine,你都应该知道它是如何结束的。启动它的同一个函数应该负责停止它。使用waitgroups或者errgroups,并且总是向一个可能被取消的函数传递一个context.Context。

7. 你不应该限制用户的选择

我们如何编写友好、灵活、强大、易用的库呢?一种方法是避免不必要地限制用户对库的操作。一个常见的Gopherism(Go主义)是 “接受接口,返回结构”。但为什么这是个好建议呢?

假设你有一个函数,接受类似于一个*os.File的参数 ,并向其写入数据。也许被写入的东西是一个文件并不重要,具体来说,它只需要是一个 “你可以写入的东西”(这个想法由标准库接口,如io.Writer表达)。有很多这样的东西:网络连接、HTTP response writer、bytes.Buffer等等。

通过强迫用户传递给你一个文件,你限制了他们对你的库的使用。通过接受一个接口(如 io.Writer)来代替,你将打开新的可能性,包括尚未被创造的类型,后续它们仍然可以满足(接口) ,可以与你的代码io.Writer一起工作。

为什么要 “返回结构体”?好吧,假设你返回一些接口类型。这极大地限制了用户对该值的操作(他们能做的就是调用其上的方法)。即使他们事实上可以用底层的具体类型做他们需要做的事情,他们也必须先用类型断言来解包它。换句话说,这就是额外的文书工作(应该避免)。

另一种避免限制用户选择的方法是不要使用只有当前Go版本才有的功能。相反,考虑至少支持最近两个主要的Go版本:有些人不能立即升级。

8. 你应该设定边界

让每一个软件组件在自己的内部是完整的、有能力的;不要让它的内部关注点暴露出来,越过它的边界渗入到其他组件中。这一点对于与其他人的代码的边界来说,是双倍的。

例如,假设你的库调用了某个API。这个API会有自己的模式和自己的词汇,反映自己的关注点和自己的领域语言。

边界是那些与你的代码接触的点:例如,调用API并解析其响应的函数。我把它称为 “airlock “函数,因为它的工作部分是确保你的内部类型和关注点不会泄露出去,并防止外来数据泄露进来。

一旦你让一点外来数据在你的程序内部自由运行,它很快就会到处乱跑。你的其他包都需要导入这些外来类型,这很烦人,而且代码将会有一股糟糕的味道。

相反,你的airlock函数应该做两件事:它应该将外来数据转化为你自己的内部格式,而且应该确保数据是有效的。现在,你的所有其他代码只需要处理你的内部类型,它不需要担心数据是否会出错、丢失或不完整。

另一种执行良好边界的方法是始终检查错误。如果你不这样做,无效的数据可能会泄露进来。

9. 你不应该在内部使用接口

一个接口值说:”我不知道这个东西到底是什么,但也许我知道有些事情我可以用它来做。” 这在Go程序中是一种超级不方便的值,因为我们不能做任何没有被接口指定的事情。

对于空接口(interface{})来说,这也是双倍的,因为我们对它一无所知。因此,根据定义,如果你有一个空的接口值,你需要把它类型化为具体的东西才能使用它。

在处理任意数据(也就是在运行时类型或模式未知的数据)时,不得不使用它们是很常见的,比如无处不在的map[string]interface{}。但是,我们应该尽快使用airlock将这一团无知转化为某种具体类型的有用的Go值。

特别是,不要用interface{}类型值来模拟泛型(Go有泛型)。不要写一个函数,接受一些可以是七种具体类型之一的值,然后对其进行类型转换,为该类型找到合适的操作。相反,写七个函数,每个具体类型一个。

不要仅因为你可以在测试中注入mock,就创建一个公共的接口,这是一个错误。创建一个真正的用户在调用你的函数之前必须实现的接口,这违反了“无文书工作原则”。不要在一般情况下写mock;Go不适合这种风格的测试。(当Go中的某些东西很困难时,这通常是你做错事的标志。)

10. 你不要盲目地遵从诫命,而要自己思考

人们说:”告诉我们什么是最佳做法”,仿佛有一本小秘籍,里面有任何技术或组织问题的正确答案。(是有的,但不要说出去。我们不希望每个人都成为顾问)。

小心任何看似清楚、明确、简单地告诉你在某种情况下该怎么做的建议。它不会适用于每一种情况,在适用的地方,它都需要告诫,需要细微的差别,需要澄清。

每个人都希望得到的是不需要真正理解就能应用的建议。但这样的建议比它能带来的帮助更危险:它能让你走到桥的一半,然后你会发现桥是纸做的,而且刚开始下雨。


非常感谢比尔-肯尼迪(Bill Kennedy)伊南克-古姆斯(Inanc Gumus)对这篇文章的有益评论。


“Gopher部落”知识星球正式转正(从试运营星球变成了正式星球)!“gopher部落”旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,>每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需>求!部落目前虽小,但持续力很强。在2021年上半年,部落将策划两个专题系列分享,并且是部落独享哦:

  • Go技术书籍的书摘和读书体会系列
  • Go与eBPF系列

欢迎大家加入!

Go技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中!本专栏主要满足广大gopher关于Go语言进阶的需求,围绕如何写出地道且高质量Go代码给出50条有效实践建议,上线后收到一致好评!欢迎大家订
阅!

img{512x368}

我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网热卖中,欢迎小伙伴们订阅学习!

img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 微信公众号:iamtonybai
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • “Gopher部落”知识星球:https://public.zsxq.com/groups/51284458844544

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats