标签 标准库 下的文章

使用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}

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

给expvarmon插上数据持久化的“翅膀”

本文永久链接 – https://tonybai.com/2021/04/14/expvarmon-save-and-convert-to-xlsx

1. expvar包与expvarmon

Go在标准库中为暴露Go应用内部指标数据提供了标准的对外接口,这就是expvar包。expvar包通过init函数将内置的expvarHandler(一个标准http HandlerFunc)注册到http包ListenAndServe创建的默认Server上。

// $GOROOT/src/expvar/expvar.go
func init() {
        http.HandleFunc("/debug/vars", expvarHandler)
        Publish("cmdline", Func(cmdline))
        Publish("memstats", Func(memstats))
}

这样如果一个Go应用要想利用expvar默认暴露的内部指标数据,仅需做到两点:

  • 以副作用方式导入expvar包
import _ "expvar"
  • 启动默认HTTP Server
http.ListenAndServe("localhost:8080", nil)

我们来建立的使用expvar包暴露指标的最简单的例子:

// expvar_demo1.go
package main

import (
    _ "expvar"
    "net/http"
)

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

这样expvar包的expvarHandler会自动响应到localhost:8080/debug/vars上的http请求:

$go build expvar_demo1.go
$./expvar_demo1 -w=1 -r=2

$curl localhost:8080/debug/vars
{
"cmdline": ["./expvar_demo1","-w=1","-r=2"],
"memstats": {"Alloc":227088,"TotalAlloc":227088,"Sys":71650320,"Lookups":0,"Mallocs":730,"Frees":13,"HeapAlloc":227088,"HeapSys":66715648,"HeapIdle":65937408,"HeapInuse":778240,"HeapReleased":65937408,"HeapObjects":717,"StackInuse":393216,"StackSys":393216,"MSpanInuse":37536,"MSpanSys":49152,"MCacheInuse":9600,"MCacheSys":16384,"BuckHashSys":3769,"GCSys":3783272,"OtherSys":688879,"NextGC":4473924,"LastGC":0,"PauseTotalNs":0,"PauseNs":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"PauseEnd":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"NumGC":0,"NumForcedGC":0,"GCCPUFraction":0,"EnableGC":true,"DebugGC":false,"BySize":[{"Size":0,"Mallocs":0,"Frees":0},{"Size":8,"Mallocs":14,"Frees":0},{"Size":16,"Mallocs":297,"Frees":0},{"Size":24,"Mallocs":32,"Frees":0},{"Size":32,"Mallocs":20,"Frees":0},{"Size":48,"Mallocs":105,"Frees":0},{"Size":64,"Mallocs":31,"Frees":0},{"Size":80,"Mallocs":9,"Frees":0},{"Size":96,"Mallocs":13,"Frees":0},{"Size":112,"Mallocs":2,"Frees":0},{"Size":128,"Mallocs":7,"Frees":0},{"Size":144,"Mallocs":3,"Frees":0},{"Size":160,"Mallocs":16,"Frees":0},{"Size":176,"Mallocs":5,"Frees":0},{"Size":192,"Mallocs":0,"Frees":0},{"Size":208,"Mallocs":33,"Frees":0},{"Size":224,"Mallocs":3,"Frees":0},{"Size":240,"Mallocs":0,"Frees":0},{"Size":256,"Mallocs":10,"Frees":0},{"Size":288,"Mallocs":8,"Frees":0},{"Size":320,"Mallocs":2,"Frees":0},{"Size":352,"Mallocs":10,"Frees":0},{"Size":384,"Mallocs":24,"Frees":0},{"Size":416,"Mallocs":7,"Frees":0},{"Size":448,"Mallocs":0,"Frees":0},{"Size":480,"Mallocs":1,"Frees":0},{"Size":512,"Mallocs":0,"Frees":0},{"Size":576,"Mallocs":3,"Frees":0},{"Size":640,"Mallocs":3,"Frees":0},{"Size":704,"Mallocs":5,"Frees":0},{"Size":768,"Mallocs":0,"Frees":0},{"Size":896,"Mallocs":7,"Frees":0},{"Size":1024,"Mallocs":7,"Frees":0},{"Size":1152,"Mallocs":10,"Frees":0},{"Size":1280,"Mallocs":4,"Frees":0},{"Size":1408,"Mallocs":1,"Frees":0},{"Size":1536,"Mallocs":0,"Frees":0},{"Size":1792,"Mallocs":5,"Frees":0},{"Size":2048,"Mallocs":1,"Frees":0},{"Size":2304,"Mallocs":2,"Frees":0},{"Size":2688,"Mallocs":2,"Frees":0},{"Size":3072,"Mallocs":0,"Frees":0},{"Size":3200,"Mallocs":0,"Frees":0},{"Size":3456,"Mallocs":0,"Frees":0},{"Size":4096,"Mallocs":4,"Frees":0},{"Size":4864,"Mallocs":0,"Frees":0},{"Size":5376,"Mallocs":1,"Frees":0},{"Size":6144,"Mallocs":1,"Frees":0},{"Size":6528,"Mallocs":0,"Frees":0},{"Size":6784,"Mallocs":0,"Frees":0},{"Size":6912,"Mallocs":0,"Frees":0},{"Size":8192,"Mallocs":1,"Frees":0},{"Size":9472,"Mallocs":0,"Frees":0},{"Size":9728,"Mallocs":0,"Frees":0},{"Size":10240,"Mallocs":8,"Frees":0},{"Size":10880,"Mallocs":0,"Frees":0},{"Size":12288,"Mallocs":0,"Frees":0},{"Size":13568,"Mallocs":0,"Frees":0},{"Size":14336,"Mallocs":0,"Frees":0},{"Size":16384,"Mallocs":0,"Frees":0},{"Size":18432,"Mallocs":0,"Frees":0}]}
}

如果我们不使用http.ListenAndServe建立的默认Server呢?expvar包也提供了相应的方法帮助你在自定义http server以及自定义请求路径上使用expvarHandler,我们看看下面示例:

// expvar_demo2.go
package main

import (
    "expvar"
    "net/http"
)

func main() {
    mux := http.NewServeMux()
    mux.Handle("/mydebug/myvars", expvar.Handler())
    var server = &http.Server{
        Addr:    "localhost:8081",
        Handler: mux,
    }

    server.ListenAndServe()
}

在这个示例中,我们利用http.ServeMux建立了expvarHandler响应的自定义路径(/mydebug/myvars),并自定义了一个http.Server,这样当expvar_demo2运行起来后,我们就可以在localhost:8081/mydebug/myvars上获取该应用暴露的指标数据了。

通过expvar_demo1的输出结果,我们看到expvar默认将命令行字段和runtime包的MemStats结构暴露给外部。我们也可以自定义要暴露到外部的数据,expvar包提供了常用指标类型的便捷接口以帮助我们更容易的自定义要暴露到外部的数据,看下面示例:

// expvar_demo3.go
package main

import (
    "expvar"
    _ "expvar"
    "net/http"
)

var (
    total *expvar.Int
)

func init() {
    total = expvar.NewInt("TotalRequest")
}

func main() {
    http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        total.Add(1)
        w.Write([]byte("hello, go\n"))
    }))
    http.ListenAndServe(":8080", nil)
}

在这个示例中,我们自定义了一个公开指标TotalRequest,用于描述该Server总共处理了多少个请求。我们用*expvar.Int作为TotalRequest的类型,expvar.Int类型提供了并发安全的Add方法,利用该方法我们可以对指标做运算。运行上面示例后,我们就可以获取TotalRequest这个指标了:

$curl localhost:8080/debug/vars
{
"TotalRequest": 2,
... ..
}

expvar包提供了对外的数据接口,但观测方式却是你决定的。图形化的观测方式是对人类友好的,一位名为divan的gopher开发了expvarmon工具,该工具可以在命令行终端以图形化的方式实时展示特定的指标数据的变化,我们可以执行如下命令实时查看应用指标变化;

$expvarmon -ports="http://localhost:8080/debug/vars" -i 1s

命令执行的效果如下:

如果不指定指标,那么expvarmon默认展示上述图中的memstats的几个指标!

expvarmon支持实时获取数据并展示数据的实时变动趋势。但有些时候,我们不仅要看实时趋势,可能还需要将数据存储起来便于事后分析。但expvarmon不支持将数据序列化到磁盘并做历史数据查看和分析。笔者曾提过issue,但作者似乎认为这个项目完成度已经很高了,该项目到2019年后就没有更新了。因此自然也没人理我的issue。我也只能自己动手丰衣足食了。

2. expvarmon的大致原理

要想基于expvarmon二次开发出支持数据持久化的版本,我们首先需要大致弄清楚expvarmon的工作原理,这里我将其工作原理大致总结为下面这幅示意图:

  • expvarmon执行时的两个命令行标志参数很重要:-ports和-vars。前者决定了expvarmon启动多少个Service(每个Service一个goroutine承载):
// https://github.com/divan/expvarmon/blob/master/main.go

        for _, port := range ports {
                service := NewService(port, vars)
                data.Services = append(data.Services, service)
        }

后者用于指定expvarmon要实时显示的数据项:

// https://github.com/divan/expvarmon/blob/master/service.go

// NewService returns new Service object.
func NewService(url url.URL, vars []VarName) *Service {
        values := make(map[VarName]*Stack)
        for _, name := range vars { //根据vars建立存储对应var数据的Stack
                values[VarName(name)] = NewStack()
        }
    ... ...
}
  • expvar定时采集各个目标app的指标数据
// https://github.com/divan/expvarmon/blob/master/service.go

func main() {
    ... ...
    UpdateAll(ui, data)
        for {
                select {
                case <-tick.C:
                        UpdateAll(ui, data)
                case e := <-termui.PollEvents():
                        if e.Type == termui.KeyboardEvent && e.ID == "q" {
                                return
                        }
                        if e.Type == termui.ResizeEvent {
                                ui.Update(*data)
                        }
                }
        }
}

// UpdateAll collects data from expvars and refreshes UI.
func UpdateAll(ui UI, data *UIData) {
        var wg sync.WaitGroup
        for _, service := range data.Services {
                wg.Add(1)
                go service.Update(&wg) // 每个服务单独获取对应port的数据
        }
        wg.Wait()

        data.LastTimestamp = time.Now()

        ui.Update(*data) // 更新并刷新命令行终端ui
}

3. 持久化到csv文件中

大致了解expvarmon的运作原理后,我们就来设计和实现将expvarmon启动后针对每个port得到的指标数据存储到磁盘文件中留待后续分析之用,这里选择持久化到csv文件中,csv文件不仅便于直接打开并肉眼查看,也便于后续转换为其他文件格式,比如:Microsoft的excel文件。

下面是对expvarmon的设计与实现改动点:

  • 增加-w命令行标志参数(布尔型),如果为true,则持久化获取到的指标数据
// https://github.com/bigwhite/expvarmon/blob/master/main.go
var (
        ... ...
        serialize = flag.Bool("w", false, "Serialize the data into a disk file")
)
  • 在Service结构中增加持久化数据所需字段
// https://github.com/bigwhite/expvarmon/blob/master/service.go

// Service represents constantly updating info about single service.
type Service struct {
        ... ...
        vars    []VarName
        // for serializing the data
        // controlled by cmd option: serialize
        f *os.File
        w *csv.Writer // csv writer
}

vars用于存储该Service对应的指标名;f为文件名;w是创建的csv.Writer结构。

  • 在创建Service的时候,根据-w的值来决定是否创建持久化文件:
// https://github.com/bigwhite/expvarmon/blob/master/service.go
func NewService(url url.URL, vars []VarName) *Service {
    ... ...
    if *serialize {
                f, err := os.Create(s.Name + ".csv")
                if err != nil {
                        panic(err)
                }
                s.f = f
                s.w = csv.NewWriter(f)

                // write first record: category line
                record := []string{"time"}
                for _, v := range vars {
                        record = append(record, string(v))
                }
                s.w.Write(record)
                s.w.Flush()
        }
        ... ...
}

我们看到:当-w为true时,NewService创建了持久化文件,并用Service的Name+.csv后缀为其命名。文件创建成功后,我们将写入第一行csv数据,这一行数据为数据类别,就像下面这样:

// 10.10.195.133:8080.csv
time,mem:memstats.Alloc,mem:memstats.Sys,mem:memstats.HeapAlloc,mem:memstats.HeapInuse,duration:memstats.PauseNs,duration:memstats.PauseTotalNs

除了第一列为时间(time)外,其余列都是以指标名命名的,如:mem:memstats.Alloc。

  • 我们在Service的Update方法中定时写入指标数据
// https://github.com/bigwhite/expvarmon/blob/master/service.go

// Update updates Service info from Expvar variable.
func (s *Service) Update(wg *sync.WaitGroup) {
        ... ...
        if *serialize {
                // serialize the values to csv
                tm := time.Now().Format("2006-01-02 15:04:05")
                values := []string{tm}
                for _, name := range s.vars {
                        values = append(values, s.Value(name))
                }
                s.w.Write(values)
                s.w.Flush()
        }
}
  • 增加Service的Close方法以优雅关闭csv文件

和原expvarmon不同的是,我们二次开发的expvarmon在-w为true时会为每个Service创建一个磁盘文件,这样我们就需要记着在适当的时候优雅的关闭这些csv格式的磁盘文件。

// https://github.com/bigwhite/expvarmon/blob/master/service.go

// Close does some cleanup before service exit
func (s *Service) Close() {
        if *serialize {
                if s.f != nil {
                        s.f.Close()
                }
        }
}

我们在程序退出前通过defer来调用Service的关闭方法:

// https://github.com/bigwhite/expvarmon/blob/master/main.go

func main() {
    ... ...
        // Init UIData
        data := NewUIData(vars)
        for _, port := range ports {
                service := NewService(port, vars)
                data.Services = append(data.Services, service)
        }

        defer func() {
                // close service before program exit
                for _, service := range data.Services {
                        service.Close()
                }
        }()
    ... ...
}

按照上述这几点改造后,我们再执行如下命令:

$expvarmon -ports="http://10.10.195.133:8080/debug/vars" -i 1s -w=true

我们将得到10.10.195.133:8080.csv文件(如果-ports由多个值组成,那么将生成多个.csv文件),内容如下:

$cat 10.10.195.133:8080.csv
time,mem:memstats.Alloc,mem:memstats.Sys,mem:memstats.HeapAlloc,mem:memstats.HeapInuse,duration:memstats.PauseNs,duration:memstats.PauseTotalNs
2021-04-09 16:55:58,15MB,88MB,15MB,25MB,159µs,1m50s
2021-04-09 16:55:59,15MB,88MB,15MB,25MB,159µs,1m50s
2021-04-09 16:56:00,15MB,88MB,15MB,25MB,159µs,1m50s
2021-04-09 16:56:01,15MB,88MB,15MB,25MB,159µs,1m50s
2021-04-09 16:56:02,15MB,88MB,15MB,25MB,159µs,1m50s
2021-04-09 16:56:03,15MB,88MB,15MB,25MB,159µs,1m50s
2021-04-09 16:56:04,15MB,88MB,15MB,25MB,159µs,1m50s
2021-04-09 16:56:05,15MB,88MB,15MB,25MB,159µs,1m50s
2021-04-09 16:56:06,15MB,88MB,15MB,25MB,159µs,1m50s
2021-04-09 16:56:07,15MB,88MB,15MB,25MB,159µs,1m50s
2021-04-09 16:56:08,16MB,88MB,16MB,25MB,159µs,1m50s
2021-04-09 16:56:09,15MB,88MB,15MB,25MB,159µs,1m50s
2021-04-09 16:56:10,15MB,88MB,15MB,25MB,159µs,1m50s
2021-04-09 16:56:11,15MB,88MB,15MB,25MB,159µs,1m50s
2021-04-09 16:56:12,15MB,88MB,15MB,25MB,159µs,1m50s
2021-04-09 16:56:13,15MB,88MB,15MB,25MB,159µs,1m50s
2021-04-09 16:56:14,15MB,88MB,15MB,25MB,159µs,1m50s
2021-04-09 16:56:15,15MB,88MB,15MB,25MB,159µs,1m50s
2021-04-09 16:56:16,15MB,88MB,15MB,25MB,159µs,1m50s
2021-04-09 16:56:17,15MB,88MB,15MB,25MB,159µs,1m50s
... ...

4. 将csv数据转换为excel图表

csv存储了各个应用暴露给外部的分时指标数据,但要对这些数据进行分析,我们需要将csv中的数据以可视化的形式展示出来,而excel图表是一个不错的选择。

为此,我建立了一个csv2xls的工具项目,专门用来将expvarmon生成的csv文件转换为excel图表。

csv2xls的用法如下:

$./csv2xls -h
Usage of ./csv2xls:
  -col int
        the column which we draw a chart based on, default: 1 (range 0~max-1) (default 1)
  -i string
        the name of csv file
  -o string
        the name of xls file

Examples:
        ./csv2xls -i xxx.csv
        ./csv2xls -i xxx.csv -o yyy.xlsx

csv2xls将csv文件中的数据读取并存储在xls中,并支持基于其中某列数据生成对应的折线图。以上面的10.10.195.133:8080.csv为例,我们通过命令:csv2xls -i 10.10.195.133:8080.csv即可生成如下excel图表文件:

csv2xls使用p著名的excelize(go语言execl操作库)](github.com/360EntSecGroup-Skylar/excelize)生成excel文件。

5. 小结

至此,我们给expvarmon插上数据持久化的“翅膀”的目的算是初步达到了。但是由于app指标数据千变万化,expvarmon使用的byten包又给解析指标数据单位带来了一些复杂性,因此csv2xls还不完善,后续还有很大的改进的空间。

  • 支持公开指标持久化的expvarmon的代码在这里(https://github.com/bigwhite/expvarmon)。
  • csv2xls的代码在这里(https://github.com/bigwhite/csv2xls)。

“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