2021年四月月 发布的文章

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}

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

使用reflect包在反射世界里读写各类型变量

本文永久链接 – https://tonybai.com/2021/mm/dd/variable-operation-using-reflection-in-go

Go在标准库中提供的reflect包让Go程序具备运行时的反射能力(reflection),但这种反射能力也是一把“双刃剑”,它在解决一类特定问题方面具有优势,但也带来了逻辑不清晰、性能问题以及难于发现问题和调试等不足。不过从Go诞生伊始就随着Go一起发布的reflect包是Go不可或缺的重要能力,不管你是否使用,都要掌握使用reflect与类型系统交互的基本方法,比如在反射的世界里如何读写各类型变量。本文就来和大家快速过一遍使用reflect包读写Go基本类型变量、复合类型变量的方法以及它们的应用。

1. 基本类型

进入reflect世界的大门主要有两个:reflect.ValueOf和reflect.TypeOf。进入到反射世界,每个变量都能找到一个与自己的对应的reflect.Value,通过该Value我们可以读写真实世界的变量信息。这里主要和大家过一遍操作各类型变量值的方法,因此主要用到的是reflect.ValueOf。

Go原生基本类型(非复合类型)主要包括:

  • 整型(int, int8, int16, int32(rune), int64, uint, uint8(byte), uint16, uint32, uint64)
  • 浮点型(float32, float64)
  • 复数类型(complex64, complex128)
  • 布尔类型(bool)
  • 字符串类型(string)

我们在反射的世界里如何获取这些类型变量的值,又或如何在反射的世界里修改这些变量的值呢?下面这个示例可以作为日常使用reflect读写Go基本类型变量的速查表:

// github.com/bigwhite/experiments/blob/master/vars-in-reflect/basic/main.go

package main

import (
    "fmt"
    "reflect"
)

func main() {
    // 整型
    var i int = 11
    vi := reflect.ValueOf(i)                         // reflect Value of i
    fmt.Printf("i = [%d], vi = [%d]\n", i, vi.Int()) // i = [11], vi = [11]
    // vi.SetInt(11 + 100) // panic: reflect: reflect.Value.SetInt using unaddressable value

    vai := reflect.ValueOf(&i) // reflect Value of Address of i
    vi = vai.Elem()
    fmt.Printf("i = [%d], vi = [%d]\n", i, vi.Int()) // i = [11], vi = [11]
    vi.SetInt(11 + 100)
    fmt.Printf("after set, i = [%d]\n", i) // after set, i = [111]

    // 整型指针
    i = 11
    var pi *int = &i
    vpi := reflect.ValueOf(pi) // reflect Value of pi
    vi = vpi.Elem()
    vi.SetInt(11 + 100)
    fmt.Printf("after set, i = [%d]\n", i) // after set, i = [111]

    // 浮点型
    var f float64 = 3.1415

    vaf := reflect.ValueOf(&f)
    vf := vaf.Elem()
    fmt.Printf("f = [%f], vf = [%f]\n", f, vf.Float()) // f = [3.141500], vf = [3.141500]
    vf.SetFloat(100 + 3.1415)
    fmt.Printf("after set, f = [%f]\n", f) // after set, f = [103.141500]

    // 复数型
    var c = complex(5.1, 6.2)

    vac := reflect.ValueOf(&c)
    vc := vac.Elem()
    fmt.Printf("c = [%g], vc = [%g]\n", f, vc.Complex()) // c = [103.1415], vc = [(5.1+6.2i)]
    vc.SetComplex(complex(105.1, 106.2))
    fmt.Printf("after set, c = [%g]\n", c) // after set, c = [(105.1+106.2i)]

    // 布尔类型
    var b bool = true

    vab := reflect.ValueOf(&b)
    vb := vab.Elem()
    fmt.Printf("b = [%t], vb = [%t]\n", b, vb.Bool()) // b = [true], vb = [true]
    vb.SetBool(false)
    fmt.Printf("after set, b = [%t]\n", b) // after set, b = [false]

    // 字符串类型
    var s string = "hello, reflect"

    vas := reflect.ValueOf(&s)
    vs := vas.Elem()
    fmt.Printf("s = [%s], vs = [%s]\n", s, vs.String()) // s = [hello, reflect], vs = [hello, reflect]
    vs.SetString("bye, reflect")
    fmt.Printf("after set, s = [%s]\n", s) // after set, s = [bye, reflect]
}

我们看到:

  • 原生基本类型变量通过reflect.ValueOf进入反射世界,如果最终要在反射世界修改原变量的值,那么传给ValueOf的不应该是变量自身,而是该变量的地址,指针类型除外。

  • 进入反射世界后,利用reflect.Value的Elem方法获取指针/地址指向的真正存储变量值的Value实例,通过Value类型提供的各种“方法糖”读取变量的值,比如:reflect.Value.Int、reflect.Value.String、reflect.Value.Bool等。

  • 同样,在反射世界中,我们通过reflect.Value的SetXXX系列方法在运行时设置相关变量的值,从而达到写变量的目的。

2. 复合类型

前面我们已经看到,使用reflect包在反射世界读写原生基本类型的变量还是相对容易的多的,接下来我们再来看看复合类型(Composite type)变量的读写。

Go中的复合类型包括:

  • 数组
  • 切片
  • map
  • 结构体
  • channel

与基本类型变量不同,复合变量多由同构和异构的字段(field)或元素(element)组成,如何读写复合类型变量中的字段或元素的值才是我们需要考虑的问题。下面这个示例可作为日常使用reflect在反射世界里读写Go复合类型变量中字段或元素值的速查表:

// github.com/bigwhite/experiments/blob/master/vars-in-reflect/composite/main.go

package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

type Foo struct {
    Name string
    age  int
}

func main() {
    // 数组
    var a = [5]int{1, 2, 3, 4, 5}
    vaa := reflect.ValueOf(&a) // reflect Value of Address of arr
    va := vaa.Elem()
    va0 := va.Index(0)
    fmt.Printf("a0 = [%d], va0 = [%d]\n", a[0], va0.Int()) // a0 = [1], va0 = [1]
    va0.SetInt(100 + 1)
    fmt.Printf("after set, a0 = [%d]\n", a[0]) // after set, a0 = [101]

    // 切片
    var s = []int{11, 12, 13}
    vs := reflect.ValueOf(s)
    vs0 := vs.Index(0)
    fmt.Printf("s0 = [%d], vs0 = [%d]\n", s[0], vs0.Int()) // s0 = [11], vs0 = [11]
    vs0.SetInt(100 + 11)
    fmt.Printf("after set, s0 = [%d]\n", s[0]) // after set, s0 = [111]

    // map
    var m = map[int]string{
        1: "tom",
        2: "jerry",
        3: "lucy",
    }

    vm := reflect.ValueOf(m)
    vm_1_v := vm.MapIndex(reflect.ValueOf(1))                      // the reflect Value of the value of key 1
    fmt.Printf("m_1 = [%s], vm_1 = [%s]\n", m[1], vm_1_v.String()) // m_1 = [tom], vm_1 = [tom]
    vm.SetMapIndex(reflect.ValueOf(1), reflect.ValueOf("tony"))
    fmt.Printf("after set, m_1 = [%s]\n", m[1]) // after set, m_1 = [tony]

    // 为map m新增一组key-value
    vm.SetMapIndex(reflect.ValueOf(4), reflect.ValueOf("amy"))
    fmt.Printf("after set, m = [%#v]\n", m) // after set, m = [map[int]string{1:"tony", 2:"jerry", 3:"lucy", 4:"amy"}]

    // 结构体
    var f = Foo{
        Name: "lily",
        age:  16,
    }

    vaf := reflect.ValueOf(&f)
    vf := vaf.Elem()
    field1 := vf.FieldByName("Name")
    fmt.Printf("the Name of f = [%s]\n", field1.String()) // the Name of f = [lily]
    field2 := vf.FieldByName("age")
    fmt.Printf("the age of f = [%d]\n", field2.Int()) // the age of f = [16]

    field1.SetString("ally")
    // field2.SetInt(8) // panic: reflect: reflect.Value.SetInt using value obtained using unexported field
    nAge := reflect.NewAt(field2.Type(), unsafe.Pointer(field2.UnsafeAddr())).Elem()
    nAge.SetInt(8)
    fmt.Printf("after set, f is [%#v]\n", f) // after set, f is [main.Foo{Name:"ally", age:8}]

    // 接口
    var g = Foo{
        Name: "Jordan",
        age:  40,
    }

    // 接口底层动态类型为复合类型变量
    var i interface{} = &g
    vi := reflect.ValueOf(i)
    vg := vi.Elem()

    field1 = vg.FieldByName("Name")
    fmt.Printf("the Name of g = [%s]\n", field1.String()) // the Name of g = [Jordan]
    field2 = vg.FieldByName("age")
    fmt.Printf("the age of g = [%d]\n", field2.Int()) // the age of g = [40]

    nAge = reflect.NewAt(field2.Type(), unsafe.Pointer(field2.UnsafeAddr())).Elem()
    nAge.SetInt(50)
    fmt.Printf("after set, g is [%#v]\n", g) // after set, g is [main.Foo{Name:"Jordan", age:50}]

    // 接口底层动态类型为基本类型变量
    var n = 5
    i = &n
    vi = reflect.ValueOf(i).Elem()
    fmt.Printf("i = [%d], vi = [%d]\n", n, vi.Int()) // i = [5], vi = [5]
    vi.SetInt(10)
    fmt.Printf("after set, n is [%d]\n", n) // after set, n is [10]

    // channel
    var ch = make(chan int, 100)
    vch := reflect.ValueOf(ch)
    vch.Send(reflect.ValueOf(22))

    j := <-ch
    fmt.Printf("recv [%d] from channel\n", j) // recv [22] from channel

    ch <- 33
    vj, ok := vch.Recv()
    fmt.Printf("recv [%d] ok[%t]\n", vj.Int(), ok) // recv [33] ok[true]
}

从上述示例,我们可以得到如下一些信息:

  • 在反射的世界里,reflect包针对复合类型中的元素或字段的读写提供了相应的方法,比如针对数组、切片元素的Value.Index,针对map key-value的Value.MapIndex,针对结构体字段的Field、FieldByName,针对channel的Send和Recv。
  • 切片、map和channel由于其底层实现为指针类型结构,我们可以直接利用其在反射世界中对应的Value在反射世界中修改其内部元素;
  • 对于结构体中的非导出字段(unexported field),我们可以读取其值,但无法直接修改其值。在上面的示例中,我们通过下面的unsafe手段实现了对其的赋值:
        nAge = reflect.NewAt(field2.Type(), unsafe.Pointer(field2.UnsafeAddr())).Elem()
        nAge.SetInt(50)

我们通过reflect.NewAt创建了一个新Value实例,该实例表示指向field2地址的指针。然后通过Elem方法,我们得到该指针Value指向的对象的Value:nAge,实际就是field2变量。然后通过nAge设置的新值也将反映在field2的值上。这和上面基本类型那个示例中的vpi和vi的功用类似。

3. 获取系统资源描述符的值

reflect包的一大功用就是获取一些被封装在底层的系统资源描述符的值,比如:socket描述符、文件描述符。

a) 文件描述符

os.File提供了Fd方法用于获取文件对应的os底层的文件描述符的值。我们也可以使用反射来实现同样的功能:

// github.com/bigwhite/experiments/blob/master/vars-in-reflect/system-resource/file_fd.go
package main

import (
    "fmt"
    "os"
    "reflect"
)

func fileFD(f *os.File) int {
    file := reflect.ValueOf(f).Elem().FieldByName("file").Elem()
    pfdVal := file.FieldByName("pfd")
    return int(pfdVal.FieldByName("Sysfd").Int())
}

func main() {
    fileName := os.Args[1]
    f, err := os.Open(fileName)
    if err != nil {
        panic(err)
    }

    defer f.Close()

    fmt.Printf("file descriptor is %d\n", f.Fd())
    fmt.Printf("file descriptor in reflect is %d\n", fileFD(f))
}

执行上述示例:

$go build file_fd.go
$./file_fd file_fd.go
file descriptor is 3
file descriptor in reflect is 3

我们看到通过reflect获取到的fd值与通过Fd方法得到的值是一致的。

下面我们可以基于上面对读写基本类型和复合类型变量的理解来简单分析一下fileFD函数的实现:

os.File的定义如下:

// $GOROOT/src/os/types.go

type File struct {
        *file // os specific
}

为了通过反射获取到未导出指针变量file,我们使用下面反射语句:

    file := reflect.ValueOf(f).Elem().FieldByName("file").Elem()

有了上面的Value实例file,我们就可以继续反射os.file结构了。os.file结构是因os而异的,以linux/mac的unix为例,os.file的结构如下:

// $GOROOT/src/os/file_unix.go

type file struct {
        pfd         poll.FD
        name        string
        dirinfo     *dirInfo // nil unless directory being read
        nonblock    bool     // whether we set nonblocking mode
        stdoutOrErr bool     // whether this is stdout or stderr
        appendMode  bool     // whether file is opened for appending
}

于是我们继续反射:

    pfdVal := file.FieldByName("pfd")

而poll.FD的结构如下:

// $GOROOT/src/internal/poll/fd_unix.go

// field of a larger type representing a network connection or OS file.
type FD struct {
        // Lock sysfd and serialize access to Read and Write methods.
        fdmu fdMutex

        // System file descriptor. Immutable until Close.
        Sysfd int

        // I/O poller.
        pd pollDesc

        // Writev cache.
        iovecs *[]syscall.Iovec 

        // Semaphore signaled when file is closed.
        csema uint32

        // Non-zero if this file has been set to blocking mode.
        isBlocking uint32

        // Whether this is a streaming descriptor, as opposed to a
        // packet-based descriptor like a UDP socket. Immutable.
        IsStream bool

        // Whether a zero byte read indicates EOF. This is false for a
        // message based socket connection.
        ZeroReadIsEOF bool

        // Whether this is a file rather than a network socket.
        isFile bool
}

这其中的Sysfd记录的就是系统的文件描述符的值,于是通过下面语句即可得到该文件描述符的值:

    return int(pfdVal.FieldByName("Sysfd").Int())

b) socket描述符

unix下一切皆文件!socket描述符也是一个文件描述符,并且Go并没有在标准库中直接提供获取socket文件描述符的API。我们只能通过反射获取。看下面示例:

// github.com/bigwhite/experiments/blob/master/vars-in-reflect/system-resource/socket_fd.go

package main

import (
    "fmt"
    "log"
    "net"
    "reflect"
)

func socketFD(conn net.Conn) int {
    tcpConn := reflect.ValueOf(conn).Elem().FieldByName("conn")
    fdVal := tcpConn.FieldByName("fd")
    pfdVal := fdVal.Elem().FieldByName("pfd")
    return int(pfdVal.FieldByName("Sysfd").Int())
}

func main() {

    ln, err := net.Listen("tcp", ":8080")
    if err != nil {
        panic(err)
    }

    for {
        conn, err := ln.Accept()
        if err != nil {
            if ne, ok := err.(net.Error); ok && ne.Temporary() {
                log.Printf("accept temp err: %v", ne)
                continue
            }

            log.Printf("accept err: %v", err)
            return
        }

        fmt.Printf("conn fd is [%d]\n", socketFD(conn))
    }
}

我们看到socketFD的实现与fileFD的实现有些类似,我们从net.Conn一步步反射得到底层的Sysfd。

传给socketFD的实参实质是一个TCPConn实例,通过reflect.ValueOf(conn).Elem()我们可以获取到该实例在反射世界的Value

// $GOROOT/src/net/tcpsock.go

type TCPConn struct {
        conn
}

然后再通过FieldByName(“conn”)得到TCPConn结构中字段conn在反射世界中的Value。net.conn结构如下:

// $GOROOT/src/net/net.go
type conn struct {
        fd *netFD
}

起哄的netFD是一个os相关的结构,以linux/mac为例,其结构如下:

// $GOROOT/src/net/fd_posix.go

// Network file descriptor.
type netFD struct {
        pfd poll.FD

        // immutable until Close
        family      int
        sotype      int
        isConnected bool // handshake completed or use of association with peer
        net         string
        laddr       Addr
        raddr       Addr
}

我们又看到了poll.FD类型字段pfd,再往下的反射就和fileFD一致了。

本文涉及的源码可以在这里下载:https://github.com/bigwhite/experiments/blob/master/vars-in-reflect


“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 Go语言编程指南
商务合作请联系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