5 4
golang reflect实践

之前看过很多Go技术的文章和一些技术群里面的交流,主流意见都是认为反射(reflect)是导致性能的主要元凶,叫开发者少用为妙;主要是他们使用反射的业务场景离不开下面的情况:

  • reflect涉及到内存分配以及后续的GC

  • reflect实现里面有大量的枚举,也就是for循环,比如类型之类的

一开始我也“遵循”的,直到最近项目需要,越来越多的场景需要用到反射机制才能灵活应对,毕竟某些情况某些情景下性能不是特别高的要求,模块的灵活性扩展性(够有能力处理各种输入类型,而这些类型可能无法共享同一个接口,也可能布局未知)才是占据重要的位置。反射不但指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力,也是元编程的一种形式,我们常见的RPC库或者一些ORM框架基本都是使用反射来实现核心的功能,所以本文主要是最近使用反射的一些总结

Go的类型系统

Go不是面向对象的语言,所以它的类型系统特点是没有泛型、没有继承的,所有的类型都是一个interface。

首先Go的类型系统主要是静态类型的,主要分两种:

  • 一种是预声明类型/pre-declared type,在创建变量的时候已经确定具体类型。go默认的有几个预声明类型:boole,num,string type。这些预声明类型被用来构造其他的类型。

  • 一种是字面量类型/type literal(array,struct,pointer,slice,map,channel,function,interface)

注意:type literal 中的struct,interface 特指匿名的类型,没有使用type包装,

每一个类型都有一个底层类型(underlying type)

在静态类型的基础上,Go提供了interface机制,它使得Go这样的静态语言拥有了一定的动态性,但却又不损失静态语言在类型安全方面拥有的编译时检查的优势。interface让我们程序设计中做到依赖于接口而不是实现,优先使用组合而不是继承,这是Go程序开发中抽象的方案。

而reflect主要是面向interface相关的类型,interface类型的变量总是具有相同的静态类型(具有某种底层类型),即使在运行时在接口变量中的值可能改变,但该值始终满足接口

Go语言中反射的操作主要定义在标准库reflect中,在标准库中定义了两种类型来表现运行时的对象信息,分别是:reflect.Type(反射对象的类型)和 reflect.Value(反射对象的值),所有反射操作都是基于这两个类型进行的。

为了说明reflect处理类型的相关知识,我们先看一个例子:

file, err := os.Open("/tmp/test.log")
if err != nil {
    panic(err)
}
defer file.Close()

var r io.Reader
r = file
fmt.Printf("%v\n", reflect.TypeOf(r))

var w io.Writer
w = r.(io.Writer)
fmt.Printf("%v\n", reflect.TypeOf(w))

程序输出

*os.File
*os.File

reflect.TypeOf 函数接受任何的interface{}参数,并且把接口中的动态类型以reflect.Type的形式返回;把一个具体指赋值给一个接口类型的时候会发生一个隐式类型的转换,转换会生成一个包含两部分内容的接口值:

因为reflect.TypeOf返回一个接口值对应的动态类型,所以它返回的总是具体类型(而不是接口类型)。比如上面例子执行输出的是*.os.File,而不是 io.Writer,这点很重要!

reflect另外一个重要操作是valueOf, 它是用来获取参数接口值得数据的动态值,当执行reflect.valueOf后,可以通过它本身的Interface()方法来获取接口的真实内容,再通过类型判断,就可以获取真实内容的值了。

Go反射的基本操作

说到Go的反射操作,先说两点:

  • 反射必须结合interface才行

  • 变量的type必须是concrete才行

interface底层有两部分:type 和 value,我们统称为pair。type是interface变量的一个指针指向的值的类型信息。value是interface变量的另外一个指针指向的实际的值

interface及其(type,value)的存在是Go反射的前提,也就是说Go的反射终归是用来检测存储在interface内部(type,value)的机制。

那么如何通过使用reflect.Value来设置值?

我们使用反射除了运行时判断真实类型,无非也是想在运行时更新变量的值

再看一个例子:

a := 12
b := reflect.ValueOf(&a)
b.Set(reflect.ValueOf(int64(48)))
fmt.Println(a)

上面的代码,我们目的是在运行时把变量a的值从12修改为48,但单当我们运行代码的时候,会抛出异常

panic: reflect: reflect.Value.Set using unaddressable value

unaddressable ? 也就是上面我们的变量b其实是unaddressable的,哪怕b有一个指针指向变量a,但无法这样的方式来更新变量a的值。

那么如何让b变为addressable呢? 再来看下面的示范:

x := 12
a := reflect.ValueOf(2)
println(a.CanAddr()) //false

b := reflect.ValueOf(x)
println(b.CanAddr()) //false

c := reflect.ValueOf(&x)
println(c.CanAddr()) //false

d := reflect.ValueOf(&x).Elem()
println(d.CanAddr()) //true

a不可取址,因为a中的值仅仅是12的拷贝的副本

b不可取址,因为b中的值是a的拷贝的副本

c不可取址,因为c中的值只是一个指针&x的拷贝

d可取址,因为Elem()方法,可以获取变量对应的可取址地址的value

官方对Elem()的解释是:

Elem returns the value that the interface v contains

所以,上面由于只有d可取值,也就是只有可寻址的reflect.Value直接调用reflect.Value.Set来更新值,否则会抛出以下的异常:

panic: reflect: reflect.Value.SetInt using unaddressable value

ok,也就是说我们可以通过Elem()来让我们某些不可取址的value变为addressable,那么我再重写一下上面的代码:

a := 12
b := reflect.ValueOf(&a).Elem()
b.Set(reflect.ValueOf(int64(48)))
fmt.Println(a)

当我们以为一切正常的时候,这个时候,我们又得到一个异常信息:

panic: reflect.Set: value of type int64 is not assignable to type int

从这个异常看出b现在是addressable了,但却变成了unassignable了!为什么呢?

我们回顾下,interface类型的变量总是具有相同的静态类型,也就是说我们通过反射来对interface进行设值,这个值的静态类型必须是相同的,否则会分配不成功,我们再更改下例子的代码:

a := 12
b := reflect.ValueOf(&a).Elem()
b.Set(reflect.ValueOf(48))
fmt.Println(a)

这个时候,变量a重要成功被更新了,所以可以总结go反射操作变量需要满足几个条件:

1、反射可以将“接口类型变量”转换为“反射类型对象”

2、反射可以将“反射类型对象”转换为“接口类型变量”

3、如果要修改“反射类型对象”,其值必须是“可写的”、“可址的”

若对go reflect 赋值操作感兴趣的,可以阅读文章《learning to use go reflection》
若要更深入了解Go语言的reflect,可以阅读官方的《laws of reflection》

反射实践

一、slice类型处理

我们尝试思考下如何可以接收任意类型slices,并统一返回[]interface{}?

也许你可能会说:“这不是很容易吗?直接处理就行了”,于是你可能会这样处理:

func handleSlice(in []interface{}) (out []interface{}) {
    out = make([]interface{}, len(in))
    for i, v := range in {
        out[i] = v
    }
    return
}

于是我们尝试把一个[]int的slice作为入参的时候,还没到运行时,我们已经收到报错信息:

cannot use slice (type []int) as type []interface {} in argument to handleSlice4

原因很简单,我们的入参类型是[]int,它只能作为一个interface的隐藏类型,但不能作为[]interface的隐藏类型,所以上面的处理方法是不恰当的,我们应该这样写:

func handleSlice(arg interface{}) (out []interface{}, ok bool) {
    argValue := reflect.ValueOf(arg)
    if argValue.Type().Kind() == reflect.Slice {
        length := argValue.Len()
        if length == 0 {
            return
        }
        ok = true
        out = make([]interface{}, length)
        for i := 0; i < length; i++ {
            out[i] = argValue.Index(i).Interface()
        }
    }
    return
}

二、类型转换

我们使用反射的一个比较常见的功能是用于类型的转换,如下面的代码片段,我们目的是要把一个未知类型的值转化为字符串并返回

stringutil.go

... ...

func ToStr(value interface{}, args ...int) (s string) {
    switch v := value.(type) {
    case bool:
        s = strconv.FormatBool(v)
    case float32:
        s = strconv.FormatFloat(float64(v), 'f', argInt(args).Get(0, -1), argInt(args).Get(1, 32))
    case float64:
        s = strconv.FormatFloat(v, 'f', argInt(args).Get(0, -1), argInt(args).Get(1, 64))
    case int:
        s = strconv.FormatInt(int64(v), argInt(args).Get(0, 10))
    case int8:
        s = strconv.FormatInt(int64(v), argInt(args).Get(0, 10))
    case int16:
        s = strconv.FormatInt(int64(v), argInt(args).Get(0, 10))
    case int32:
        s = strconv.FormatInt(int64(v), argInt(args).Get(0, 10))
    case int64:
        s = strconv.FormatInt(v, argInt(args).Get(0, 10))
    case uint:
        s = strconv.FormatUint(uint64(v), argInt(args).Get(0, 10))
    case uint8:
        s = strconv.FormatUint(uint64(v), argInt(args).Get(0, 10))
    case uint16:
        s = strconv.FormatUint(uint64(v), argInt(args).Get(0, 10))
    case uint32:
        s = strconv.FormatUint(uint64(v), argInt(args).Get(0, 10))
    case uint64:
        s = strconv.FormatUint(v, argInt(args).Get(0, 10))
    case string:
        s = v
    case []byte:
        s = string(v)
    default:
        s = fmt.Sprintf("%v", v)
    }
    return s
}

... ...

再比方说我们日常最常用的“技巧” --- 字符串与byte数组互相转换

func BytesToString(b []byte) string {
    bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
    sh := reflect.StringHeader{bh.Data, bh.Len}
    return *(*string)(unsafe.Pointer(&sh))
}

func StringToBytes(s string) []byte {
    sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
    bh := reflect.SliceHeader{sh.Data, sh.Len, 0}
    return *(*[]byte)(unsafe.Pointer(&bh))
}

上面例子两个 reflect 例子, 我们以 BytesToString为测试对象,看下一些基准测试:

func BenchmarkDirectConvert(b *testing.B) {
    for i := 0; i < b.N; i++ {
        b := []byte("I am gopher")
        _ = string(b)
    }
}

func BenchmarkReflectConvert(b *testing.B) {
    for i := 0; i < b.N; i++ {
        b := []byte("I am gopher")
        _ = BytesToString(b)
    }
}

//运行结果:

$ go test -v -bench=. -benchmem

BenchmarkDirectConvert-4        100000000            10.8 ns/op           0 B/op           0 allocs/op
BenchmarkReflectConvert-4       200000000             6.78 ns/op           0 B/op           0 allocs/op

可以看到reflect的使用在某些情况下,非但没有引起性能问题,侧面还提升了呢,所以避开场景让reflect背上性能的锅都是不客观的。

三、单机版的MapReduce

下面是通过反射实现一个简单的单机版简单的类MR演示

package main

import (
    "fmt"
    "reflect"
)

func Map(slice interface{}, fn func(a interface{}) interface{}) interface{} {
    val := reflect.ValueOf(slice)
    out := reflect.MakeSlice(reflect.TypeOf(slice), val.Len(), val.Cap())
    for i := 0; i < val.Len(); i++ {
        ret := fn(val.Index(i).Interface())
        out.Index(i).Set(reflect.ValueOf(ret))
    }
    return out.Interface()
}

func main() {
    a := []int{1, 2, 3}
    fn := func(a interface{}) interface{} {
        return a.(int) * 2
    }
    b := Map(a, fn)
    fmt.Printf("%v\n", b)
}

四、reflect 缓存

实现reflect.Type池主要的为了某程度提升reflect的性能,避免重复操作耗时

下面是一个屏蔽用户名字展示的测试例子:

type User struct {
    Name string
    Age  int
}

var handler = func(u *User, message string) {
    fmt.Printf("Hello, My name is %s, I am %d years old ! so, %s\n", u.Name, u.Age, message)
}

//使用普通反射的方式处理名字屏蔽
func filtName(u *User, message string) {
    fn := reflect.ValueOf(handler)
    uv := reflect.ValueOf(u)
    name := uv.Elem().FieldByName("Name")
    name.SetString("XXX")
    fn.Call([]reflect.Value{uv, reflect.ValueOf(message)})
}


//重用部分数据减少重复创建的反射方式处理名字屏蔽
var offset uintptr = 0xFFFF
func filtNameWithReuseOffset(u *User, message string) {
    if offset == 0xFFFF {
        t := reflect.TypeOf(u).Elem()
        name, _ := t.FieldByName("Name")
        offset = name.Offset

    }
    up := (*[2]uintptr)(unsafe.Pointer(&u))
    upnamePtr := (*string)(unsafe.Pointer(up[0] + offset))
    *upnamePtr = "YYY"
    fn := reflect.ValueOf(handler)
    uv := reflect.ValueOf(u)
    fn.Call([]reflect.Value{uv, reflect.ValueOf(message)})
}

//压测数据:

BenchmarkFiltName-4              5000000               376 ns/op              56 B/op          3 allocs/op
BenchmarkFiltNameWithCache-4     5000000               268 ns/op              48 B/op          2 allocs/op

虽然不明显,但通过对部分数据的重用可以起到优化反射的作用,优化效果还是要看具体的业务实现常见, 当然如果可以把上面的缓存方法设计得更通用些,可以这样:

var cache = map[*uintptr]map[string]uintptr{}

func filtNameWithCache(u *User, message string) {
    itab := *(**uintptr)(unsafe.Pointer(&u))

    m, ok := cache[itab]
    if !ok {
        m = make(map[string]uintptr)
        cache[itab] = m
    }

    offset, ok := m["Name"]
    if !ok {
        t := reflect.TypeOf(u).Elem()
        name, _ := t.FieldByName("Name")
        offset = name.Offset
        m["Name"] = offset
    }
    up := (*[2]uintptr)(unsafe.Pointer(&u))
    upnamePtr := (*string)(unsafe.Pointer(up[0] + offset))
    *upnamePtr = "ZZZ"
    fn := reflect.ValueOf(handler)
    uv := reflect.ValueOf(u)
    fn.Call([]reflect.Value{uv, reflect.ValueOf(message)})

}

总结

Go的反射(reflect)是一个强大并富有表达力的工具,一般情况下它应该被小心地使用,原因有几点:

  • 如果通过反射实现的功能,则是在真正运行到的时候才会抛出panic异常

  • 反射的操作不能做静态类型检查,而且大量反射的代码可读性较差。

  • 一般场景基于反射的代码通常比正常的代码运行速度要慢

不过,关于上面第三点,尽管反射存在性能问题,但依然被频繁使用,以弥补静态语言在动态行为上的不足。某些时候,我们可以配合一些手段,以提升性能

4 27
如何把panic信息重定向

根据“墨菲定律”,我们编写的后台的服务都有出现crash的可能,一种情况是Go的后台服务我们经常也会遇到panic的情况。出问题不可怕,我们需要分析并解决问题,不过panic处理的信息,默认是直接标准输出的,我们希望能捕获它指向我们特定的文件以便能做后续问题的跟踪排查,而不是一次性输出难以跟踪。

我们一个通用的方法是

err := execFunc()
if err != nil {
    outputToFile(err)
}

但有一些第三方库会使用panic/recover机制作为其内部的异常控制方式,这样我们在外面是难以察觉的,异常信息可能就直接打到我们的标准输出那里了,除非你在执行程序之前,使用类似linux的 ./test >> panic.log ,否则我们会很大机会与重要的跟踪信息擦肩而过。(跨平台到windows可能不适用)

所以,如何把panic的信息灵活地“重定向”呢?

实现思路一般是:

1、既然panic使用的的是标准输出,我们可以使用自定义的文件file引用取代go里头的os.Stdout 和 os.Stderr

2、引起panic并测试重定向的正确性

3、windows里面没有stdout和stderr的输出方式,也没办法像unix那样使用“>>”进行标准输出的重新向,这个如何破?

我们先试试一个简单的例子:

package main

import (
    "fmt";
    "os";
)

const panicFile = "/tmp/panic.log"

func InitPanicFile() error {
    log.Println("init panic file in unix mode")
    file, err := os.OpenFile(panicFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
    if err != nil {
        return err
    }

    os.Stdout = file
    os.Stderr = file
    return nil
}

func init() {
    err := pc.InitPanicFile()
    if err != nil {
        println(err)
    }
}

func testPanic() {
    panic("test panic")
}

func main() {
    testPanic()
}

这个例子,我们尝试使用 os.Stdout = fileos.Stderr = file 来“强制”转换,但我们运行程序后,发现不起作用, /tmp/panic.log 没有任何信息流入,panic信息照样输出到标准输出那里。

关于原因,Rob是这样说的:

rob say

看来是把变量直接赋值到底层是不行的,图上所说,推荐使用syscall.Dup的方式。我们再改写下 InitPanicFile() 函数:

func InitPanicFile() error {
    log.Println("init panic file in unix mode")
    file, err := os.OpenFile(panicFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
    if err != nil {
        println(err)
        return err
    }
    if err = syscall.Dup2(int(file.Fd()), int(os.Stderr.Fd())); err != nil {
        return err
    }
    return nil
}

我们运行程序,发现panic正常定向到我们的文件里面去了:

$ tail -f /tmp/panic.log

panic: test panic

goroutine 1 [running]:

... ...
... ...

不过经过实践,上面的代码是有些bug的,原因是我们上面的file是一个局部变量,放系统发生gc的时候,会触发file里面的 runtime.SetFinalizer(f.file, (*file).close), 会引起句柄会被回收, 如果我们代码是长期运行在后台的话,建议代码调整如下的形式:

var globalFile *os.File

func InitPanicFile() error {
    log.Println("init panic file in unix mode")
    file, err := os.OpenFile(panicFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
    globalFile = file
    if err != nil {
        println(err)
        return err
    }
    if err = syscall.Dup2(int(file.Fd()), int(os.Stderr.Fd())); err != nil {
        return err
    }
    return nil
}

接下来,我们要延伸思考下,如果服务是运行在windows上面该如何破?

使用syscall.Dup2的例子windows下会编译直接报错:

undefined: syscall.Dup2

... ...

syscall.Dup2 is a linux/OSX only thing. there's no windows equivalent。

记得我前面的文件,介绍过go调用DLL的方法 《使用Go结合windows dll开发程序》 ,其实我们也可以想到,可以直接使用DLL的调用达到功能效果:

代码如下:

package main

import (
    "log"
    "os"
    "syscall"
)

const (
    kernel32dll = "kernel32.dll"
)

const panicFile = "C:/panic.log"

var globalFile *os.File

func InitPanicFile() error {
    log.Println("init panic file in windows mode")
    file, err := os.OpenFile(panicFile, os.O_CREATE|os.O_APPEND, 0666)
    globalFile = file
    if err != nil {
        return err
    }
    kernel32 := syscall.NewLazyDLL(kernel32dll)
    setStdHandle := kernel32.NewProc("SetStdHandle")
    sh := syscall.STD_ERROR_HANDLE
    v, _, err := setStdHandle.Call(uintptr(sh), uintptr(file.Fd()))
    if v == 0 {
        return err
    }
    return nil
}

func init() {
    err := pc.InitPanicFile()
    if err != nil {
        println(err)
    }
}

func testPanic() {
    panic("test panic")
}

func main() {
    testPanic()
}

然后我们把编译后的代码在windows下运行,panic信息也能正常重定向到指定文件上了。

4 24
使用Go开发一个简单反向代理服务

最近,团队的小伙伴反映,我们这边一个短连接服务在一台普通的服务器上吞吐量受到限制,所以把服务迁移到高性能机器上,虽然硬件是数倍的提升但压测发现吞吐量并没有预期的效果。

结合后台服务本身的特点初步原因分析:

  • 1、从下往上看:服务属于计算IO密集型,性能瓶颈多在于计算请求,但高配机压测过程中,受到单实例模块之间通讯采用串行调用的特点,虽然单点请求计算性能有很大提速,但总体并行上不去,CPU利用率低

  • 2、从上往下看: 吞吐量受服务器的接受能力影响很大,由于短连接接入层目前只有一个实例,无论部署在中配或是高配,除非是多实例模式或者类似nginx这种多worker工作模型,一般情况下,单实例accept的效果有限,高并发时容易成为瓶颈

  • 3、从服务进程的角度看,单个web api的请求accept队列(backlog)是有限制的,如果多实例部署也许能补短。

分析到这里,很多人都想到可以通过扩容+分布式通讯的方式来弥补短板。是的,方法是摆在面前,但是你想到一个方法不难,难的是你要如何去验证你的想法。毕竟对于一个成熟的产品技术框架,不是随便都能重构的,一定要数据说话。

不过如何调优不是本文的目的,本文的目的是如何使用Go来快速实现一个反向代理服务来验证前面的背景想法。

设计

一个反向代理层,无论是四层还是七层,我觉得实现上主要需要具备以下工作:

  • 负载均衡算法
  • 请求可传递
  • endpoints可权重配置
  • endpoints故障处理

关于使用Go写负载均衡算法,之前在 《关于Round-Robin》这文章提及过,这里不延伸。

以http为例,go如何快速实现反向代理?

查看go的文档,发现源码net/http/httputil提供了一个叫 ReverseProxy:https://godoc.org/net/http/httputil#ReverseProxy 的玩意,这个就是golang自带反向代理功能,而且使用很简单

ReverseProxy提供了ServerHTTP方法,这意味着我们可以跟普通http handler一样简单地使用它来处理请求

ReverseProxy 暴露了NewSingleHostReverseProxy的方法

// NewSingleHostReverseProxy returns a new ReverseProxy that rewrites
// URLs to the scheme, host, and base path provided in target. If the
// target's path is "/base" and the incoming request was for "/dir",
// the target request will be for /base/dir.
func NewSingleHostReverseProxy(target *url.URL) *ReverseProxy {
        targetQuery := target.RawQuery
        director := func(req *http.Request) {
                req.URL.Scheme = target.Scheme
                req.URL.Host = target.Host
                req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
                if targetQuery == "" || req.URL.RawQuery == "" {
                        req.URL.RawQuery = targetQuery + req.URL.RawQuery
                } else {
                        req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
                }
        }
        return &ReverseProxy{Director: director}
}

这样,我们可以通过一行代码就基本上实现了主体的反向代理功能了,如下:

httputil.NewSingleHostReverseProxy(address)

实现

结合Round-Robin,我们尝试实现我们的反向代理层

带权重的负载均衡实现 round-robin.go

package roundrobin

// RR: 基于 权重round robin算法的接口
type RR interface {
    Next() interface{}
    Add(node interface{}, weight int)
    RemoveAll()
    Reset()
}

const (
    RR_NGINX = 0 //Nginx算法
    RR_LVS   = 1 //LVS算法
)

//算法实现工厂类
func NewWeightedRR(rtype int) RR {
    if rtype == RR_NGINX {
        return &WNGINX{}
    } else if rtype == RR_LVS {
        return &WLVS{}
    }
    return nil
}

//节点结构
type WeightNginx struct {
    Node            interface{}
    Weight          int
    CurrentWeight   int
    EffectiveWeight int
}

func (ww *WeightNginx) fail() {
    ww.EffectiveWeight -= ww.Weight
    if ww.EffectiveWeight < 0 {
        ww.EffectiveWeight = 0
    }
}

//nginx算法实现类
type WNGINX struct {
    nodes []*WeightNginx
    n     int
}

//增加权重节点
func (w *WNGINX) Add(node interface{}, weight int) {
    weighted := &WeightNginx{
        Node:            node,
        Weight:          weight,
        EffectiveWeight: weight}
    w.nodes = append(w.nodes, weighted)
    w.n++
}

func (w *WNGINX) RemoveAll() {
    w.nodes = w.nodes[:0]
    w.n = 0
}

//下次轮询事件
func (w *WNGINX) Next() interface{} {
    if w.n == 0 {
        return nil
    }
    if w.n == 1 {
        return w.nodes[0].Node
    }

    return nextWeightedNode(w.nodes).Node
}

func nextWeightedNode(nodes []*WeightNginx) (best *WeightNginx) {
    total := 0

    for i := 0; i < len(nodes); i++ {
        w := nodes[i]

        if w == nil {
            continue
        }

        w.CurrentWeight += w.EffectiveWeight
        total += w.EffectiveWeight
        if w.EffectiveWeight < w.Weight {
            w.EffectiveWeight++
        }

        if best == nil || w.CurrentWeight > best.CurrentWeight {
            best = w
        }
    }

    if best == nil {
        return nil
    }
    best.CurrentWeight -= total
    return best
}

func (w *WNGINX) Reset() {
    for _, s := range w.nodes {
        s.EffectiveWeight = s.Weight
        s.CurrentWeight = 0
    }
}

//节点结构
type WeightLvs struct {
    Node   interface{}
    Weight int
}

//lvs算法实现类
type WLVS struct {
    nodes []*WeightLvs
    n     int
    gcd   int //通用的权重因子
    maxW  int //最大权重
    i     int //被选择的次数
    cw    int //当前的权重值
}

//下次轮询事件
func (w *WLVS) Next() interface{} {
    if w.n == 0 {
        return nil
    }

    if w.n == 1 {
        return w.nodes[0].Node
    }

    for {
        w.i = (w.i + 1) % w.n
        if w.i == 0 {
            w.cw = w.cw - w.gcd
            if w.cw <= 0 {
                w.cw = w.maxW
                if w.cw == 0 {
                    return nil
                }
            }
        }
        if w.nodes[w.i].Weight >= w.cw {
            return w.nodes[w.i].Node
        }
    }
}

//增加权重节点
func (w *WLVS) Add(node interface{}, weight int) {
    weighted := &WeightLvs{Node: node, Weight: weight}
    if weight > 0 {
        if w.gcd == 0 {
            w.gcd = weight
            w.maxW = weight
            w.i = -1
            w.cw = 0
        } else {
            w.gcd = gcd(w.gcd, weight)
            if w.maxW < weight {
                w.maxW = weight
            }
        }
    }
    w.nodes = append(w.nodes, weighted)
    w.n++
}

func gcd(x, y int) int {
    var t int
    for {
        t = (x % y)
        if t > 0 {
            x = y
            y = t
        } else {
            return y
        }
    }
}
func (w *WLVS) RemoveAll() {
    w.nodes = w.nodes[:0]
    w.n = 0
    w.gcd = 0
    w.maxW = 0
    w.i = -1
    w.cw = 0
}
func (w *WLVS) Reset() {
    w.i = -1
    w.cw = 0
}

主体部分 main.go

var RR = rr.NewWeightedRR(rr.RR_NGINX)

type handle struct {
    addrs []string
}

func (this *handle) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    addr := RR.Next().(string)
    remote, err := url.Parse("http://" + addr)
    if err != nil {
        panic(err)
    }
    proxy := httputil.NewSingleHostReverseProxy(remote)
    proxy.ServeHTTP(w, r)
}

func startServer() {
    //被代理的服务器host和port
    h := &handle{}
    h.addrs = []string{"172.17.0.2:28080", "172.17.0.3:28080"}

    w := 1
    for _, e := range h.addrs {
        RR.Add(e, w)
        w++
    }
    err := http.ListenAndServe(":28080", h)
    if err != nil {
        log.Fatalln("ListenAndServe: ", err)
    }
}

func main() {
    startServer()
}

在ReverseProxy中的ServeHTTP方法实现了这个具体的过程,主要是对源http包头进行重新封装,而后发送到后端服务器。

这样,我们一个简单快速的反向代理层就实现了,日常可以基于它自定义负载我们的服务。

4 4
使用Go结合windows dll开发程序

Go程序有一个优点是很好地做到”跨平台”,一般开发的情况,我们使用内置的相关模块实现相关功能,通过 GOOS=操作系统代号 go build 就能编译出对应平台的二进制文件. 然后把二进制文件扔往对应的服务器无论是linux或windows, 基本都能正常运行.

但是Go也不是完全的跨平台,个别情况下并没有提供Windows下的相关方法,只能通过syscall包去调用Win库.

本文主要说说我们做跨平台的时候,针对windows的一些处理: 一种常见的方式是我们需要在go的代码里面使用个别的dll文件

DLL是微软公司在微软视窗操作系统中实现共享函数库概念的一种实现方式。这些库函数的扩展名是.DLL、.OCX(包含ActiveX控制的库)或者.DRV(旧式的系统驱动程序)。

所谓动态链接(.DLL,Linux下.so文件),就是把一些经常会共用的代码(静态链接的OBJ程序库)制作成DLL,当可执行文件调用到DLL档内的函数时,Windows操作系统才会把DLL档加载内存内,DLL档本身的结构就是可执行档,当程序有需求时函数才进行链接。通过动态链接方式,内存浪费的情形将可大幅降低。静态链接库则是直接链接到可执行文件。

要解决空间浪费和更新困难这两个问题最简单的办法就是把程序的模块相互分割开来,形成独立的文件,而不再将它们静态地链接在一起。简单地讲,就是不对那些组成程序的目标文件进行链接,等到程序要运行时才进行链接。也就是说,把链接这个过程推迟到了运行时再进行,这就是动态链接(Dynamic Linking)的基本思想

要想了解Go的DLL编程,可以先阅读golang官方的相关资料 : WindowsDLLs

若要深入学习动态链接的相关背景和步骤,可以阅读《[程序员的自我修养]》

什么是syscall

在电脑中,系统调用(英语:system call),又称为系统呼叫,指运行在用户空间的程序向操作系统内核请求需要更高权限运行的服务。系统调用提供用户程序与操作系统之间的接口。大多数系统交互式操作需求在内核态执行。如设备IO操作或者进程间通信。”

实际上,你基本上做任何事情的时候,都需要系统调用。

  • 访问文件
  • 访问设备
  • 进程管理
  • 通讯
  • 时间 …

无论你是写C程序、写Go程序或者哪怕是写bash脚本,你实际上都会用到syscall

举一个简单的Go的例子 hello.go

package main
import "fmt"
func main() {
    fmt.Println("Hello, Tencent!")
}

如果我们在 Linux 上构建,并且使用 strace 的话,就可以看到发生了多少系统调用了:

$ go build hello.go
$ strace ./hello
execve("./hello", ["./hello"], [/* 23 vars */]) = 0
arch_prctl(ARCH_SET_FS, 0x52c008)       = 0
sched_getaffinity(0, 8192, [0])         = 8
mmap(0xc000000000, 65536, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xc000000000
munmap(0xc000000000, 65536)             = 0
...
futex(0x52c0b0, FUTEX_WAIT, 0, NULL)    = 0
mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f1eef3ac000
write(1, "Hello, Tencent!\n", 18Hello, Tencent!
)     = 18
futex(0x52ba58, FUTEX_WAKE, 1)          = 1
futex(0x52b990, FUTEX_WAKE, 1)          = 1
exit_group(0)                           = ?
+++ exited with 0 +++

可以关注一下最后的那个 write(), 那里就发生了syscall. 我们这里不关心相关底层细节,我们知道我们每天的编程其实都和syscall在互动

如何操作 syscall

syscall() saves CPU registers before making the system call, restores the registers upon return from the system call, and stores any error code returned by the system call in errno(3) if an error occurs.

就是说调用 syscall 之前先保存环境;syscall 返回之后,恢复环境;错误代码在 errno 中

以 sys_write 调用为例,系统调用号放到了 %rax 之中,文件的 fd 放到了 %rdi 中,要写入的 buf 放到了 %rsi 中,写入长度放到了 %rdx 之中。

当执行了 syscall() 后,开始进入 Trap,然后进入内核态,开始执行对应的系统调用的代码。而系统调用的返回值,会放到了 %rax 之中。

syscall vs. windows

举个例子,如果我们的程序平常运行在linux,为了做到程序的可可移植性, 我们会或多或少地调用winodws的相关API

下面我就通过一份代码,来说明Go在windows下运用dll,获取各个盘区的容量信息:

package main

import (
    "errors"
    "fmt"
    "syscall"
    "unsafe"
)

var kernel32 syscall.Handle

//初始化获取方法的引用
func init() {
    var err error
    kernel32, err = syscall.LoadLibrary("kernel32.dll")
    if err != nil {
        panic("获取方法应用错误")
    }

}

func getDriveNames() ([]string, error) {

    drives := []string{}

    LongPtr_DriveBuf := make([]byte, 256)

    getDrivesStringsEx, err := syscall.GetProcAddress(kernel32, "GetLogicalDriveStringsW")
    if err != nil {
        return nil, errors.New("call GetLogicalDriveStringsW fail")
    }

    //执行调用
    // 因为有2个参数,所以使用syscall就能放得下,最后的参数补0
    r, _, errno := syscall.Syscall(uintptr(getDrivesStringsEx), 2,
        uintptr(len(LongPtr_DriveBuf)),
        uintptr(unsafe.Pointer(&LongPtr_DriveBuf[0])), 0)

    if r != 0 {

        for _, v := range LongPtr_DriveBuf {
            if v < 65 || v > 90 {
                continue
            }
            //println(string(v))
            drives = append(drives, string(v)+":")
        }

    } else {
        return nil, errors.New(errno.Error())
    }

    return drives, nil
}

func getDiskGreeSpace(diskName string) {

    //将磁盘的名称转化为*UTF16
    diskNameUTF16Ptr, _ := syscall.UTF16PtrFromString(diskName)

    //使用长指针
    LongPtr_FreeBytesAvailable := int64(0)     //剩余空间
    LongPtr_TotalNumberOfBytes := int64(0)     //总空间
    LongPtr_TotalNumberOfFreeBytes := int64(0) //可用空间

    //获取方法的引用
    kernel32, err := syscall.LoadLibrary("kernel32.dll")
    if err != nil {
        panic("获取方法应用错误")
    }

    //释放方法引用
    defer syscall.FreeLibrary(kernel32)

    getDiskFreeSpaceEx, err := syscall.GetProcAddress(kernel32, "GetDiskFreeSpaceExW")
    if err != nil {
        panic("call GetZDiskFreeSpaceExW fail")
    }

    //执行调用
    // 因为有四个参数,所以使用syscall6才能放得下,最后两个参数补0
    r, _, errno := syscall.Syscall6(uintptr(getDiskFreeSpaceEx), 4,
        uintptr(unsafe.Pointer(diskNameUTF16Ptr)),
        uintptr(unsafe.Pointer(&LongPtr_FreeBytesAvailable)),
        uintptr(unsafe.Pointer(&LongPtr_TotalNumberOfBytes)),
        uintptr(unsafe.Pointer(&LongPtr_TotalNumberOfFreeBytes)),
        0, 0)

    if r != 0 {
        fmt.Printf(">>>> %s 的空间情况\n", diskName)
        fmt.Printf("剩余空间:%d G\n", LongPtr_FreeBytesAvailable/1024/1024/1024)
        fmt.Printf("用户可用空间:%d G\n", LongPtr_TotalNumberOfBytes/1024/1024/1024)
        fmt.Printf("剩余可用空间:%d G\n", LongPtr_TotalNumberOfFreeBytes/1024/1024/1024)

    } else {
        //此处的errno不是error接口,而是 type Errorno uintptr
        panic(errno)
    }
}

func main() {
    //释放方法引用
    defer syscall.FreeLibrary(kernel32)

    drives, err := getDriveNames()
    if err != nil {
        panic(err)
    }

    for _, d := range drives {
        //获取磁盘可用空间
        getDiskGreeSpace(d)
    }
}

为了可以丰富功能,做到更好的可移植性, 大家日常可以多看 MSDN文档 https://msdn.microsoft.com/en-us/library/windows/desktop/hh447209(v=vs.85)

后一页