Golang工程经验(下)

原文作者:吴德宝AllenWu

接上文:Golang工程经验

开关策略、降级策略

线上服务端系统,必须要有降级机制,也最好能够有开关机制。降级机制在于出现异常情况能够舍弃某部分服务保证其他主线服务正常;开关也有着同样的功效,在某些情况下打开开关,则能够执行某些功能或者说某套功能,关闭开关则执行另外一套功能或者不执行某个功能。

这不是Golang的语言特性,但是是工程项目里面必要的,在Golang项目中的具体实现代码片段如下:

  1package switches
  2
  3
  4var (
  5    xxxSwitchManager = SwitchManager{switches: make(map[string]*Switch)}
  6
  7   AsyncProcedure = &Switch{Name: "xxx.msg.procedure.async", On: true}
  8
  9    // 使能音视频
 10    EnableRealTimeVideo = &Switch{Name: "xxx.real.time.video", On: true}
 11
 12)
 13
 14func init() {
 15    xxxSwitchManager.Register(AsyncProcedure, 
 16    EnableRealTimeVideo)
 17}
 18
 19
 20// 具体实现结构和实现方法
 21type Switch struct {
 22    Name      string
 23    On        bool
 24    listeners []ChangeListener
 25}
 26
 27func (s *Switch) TurnOn() {
 28    s.On = true
 29    s.notifyListeners()
 30}
 31
 32func (s *Switch) notifyListeners() {
 33    if len(s.listeners) > 0 {
 34        for _, l := range s.listeners {
 35            l.OnChange(s.Name, s.On)
 36        }
 37    }
 38}
 39
 40func (s *Switch) TurnOff() {
 41    s.On = false
 42    s.notifyListeners()
 43}
 44
 45func (s *Switch) IsOn() bool {
 46    return s.On
 47}
 48
 49func (s *Switch) IsOff() bool {
 50    return !s.On
 51}
 52
 53func (s *Switch) AddChangeListener(l ChangeListener) {
 54    if l == nil {
 55        return
 56    }
 57    s.listeners = append(s.listeners, l)
 58}
 59
 60type SwitchManager struct {
 61    switches map[string]*Switch
 62}
 63
 64func (m SwitchManager) Register(switches ...*Switch) {
 65    for _, s := range switches {
 66        m.switches[s.Name] = s
 67    }
 68}
 69
 70func (m SwitchManager) Unregister(name string) {
 71    delete(m.switches, name)
 72}
 73
 74func (m SwitchManager) TurnOn(name string) (bool, error) {
 75    if s, ok := m.switches[name]; ok {
 76        s.TurnOn()
 77        return true, nil
 78    } else {
 79        return false, errors.New("switch " + name + " is not registered")
 80    }
 81}
 82
 83func (m SwitchManager) TurnOff(name string) (bool, error) {
 84    if s, ok := m.switches[name]; ok {
 85        s.TurnOff()
 86        return true, nil
 87    } else {
 88        return false, errors.New("switch " + name + " is not registered")
 89    }
 90}
 91
 92func (m SwitchManager) IsOn(name string) (bool, error) {
 93    if s, ok := m.switches[name]; ok {
 94        return s.IsOn(), nil
 95    } else {
 96        return false, errors.New("switch " + name + " is not registered")
 97    }
 98}
 99
100func (m SwitchManager) List() map[string]bool {
101    switches := make(map[string]bool)
102    for name, switcher := range m.switches {
103        switches[name] = switcher.On
104    }
105    return switches
106}
107
108type ChangeListener interface {
109    OnChange(name string, isOn bool)
110}
111
112
113// 这里开始调用
114if switches.AsyncProcedure.IsOn() {
115    // do sth
116}else{
117    // do other sth
118}

prometheus + grafana

prometheus + grafana 是业界常用的监控方案,prometheus进行数据采集,grafana进行图表展示。

Golang里面prometheus进行数据采集非常简单,有对应client库,应用程序只需暴露出http接口即可,这样,prometheus server端就可以定期采集数据,并且还可以根据这个接口来监控服务端是否异常【如挂掉的情况】。

1import  "github.com/prometheus/client_golang/prometheus"
2
3engine.GET("/metrics", gin.WrapH(prometheus.Handler()))

这样就实现了数据采集,但是具体采集什么样的数据,数据从哪里生成的,还需要进入下一步:

 1package prometheus
 2
 3import "github.com/prometheus/client_golang/prometheus"
 4
 5var DefaultBuckets = []float64{10, 50, 100, 200, 500, 1000, 3000}
 6
 7var MySQLHistogramVec = prometheus.NewHistogramVec(
 8    prometheus.HistogramOpts{
 9        Namespace: "allen.wu",
10        Subsystem: "xxx",
11        Name:      "mysql_op_milliseconds",
12        Help:      "The mysql database operation duration in milliseconds",
13        Buckets:   DefaultBuckets,
14    },
15    []string{"db"},
16)
17
18var RedisHistogramVec = prometheus.NewHistogramVec(
19    prometheus.HistogramOpts{
20        Namespace: "allen.wu",
21        Subsystem: "xxx",
22        Name:      "redis_op_milliseconds",
23        Help:      "The redis operation duration in milliseconds",
24        Buckets:   DefaultBuckets,
25    },
26    []string{"redis"},
27)
28
29func init() {
30    prometheus.MustRegister(MySQLHistogramVec)
31    prometheus.MustRegister(RedisHistogramVec)
32    ...
33}
34
35
36// 使用,在对应的位置调用prometheus接口生成数据
37instanceOpts := []redis.Option{
38        redis.Shards(shards...),
39        redis.Password(viper.GetString(conf.RedisPrefix + name + ".password")),
40        redis.ClusterName(name),
41        redis.LatencyObserver(func(name string, latency time.Duration) {
42            prometheus.RedisHistogramVec.WithLabelValues(name).Observe(float64(latency.Nanoseconds()) * 1e-6)
43        }),
44    } 

捕获异常 和 错误处理

panic 异常

捕获异常是否有存在的必要,根据各自不同的项目自行决定,但是一般出现panic,如果没有异常,那么服务就会直接挂掉,如果能够捕获异常,那么出现panic的时候,服务不会挂掉,只是当前导致panic的某个功能,无法正常使用,个人建议还是在某些有必要的条件和入口处进行异常捕获。

常见抛出异常的情况:数组越界、空指针空对象,类型断言失败等;Golang里面捕获异常通过 defer + recover来实现

C++有try。。。catch来进行代码片段的异常捕获,Golang里面有recover来进行异常捕获,这个是Golang语言的基本功,是一个比较简单的功能,不多说,看代码

 1func consumeSingle(kafkaMsg *sarama.ConsumerMessage) {
 2    var err error
 3    defer func() {
 4        if r := recover(); r != nil {
 5            if e, ok := r.(error); ok {
 6                // 异常捕获的处理
 7            }
 8        }
 9    }()
10}

在请求来源入口处的函数或者某个方法里面实现这么一段代码进行捕获,这样,只要通过这个入口出现的异常都能被捕获,并打印详细日志

error 错误

error错误,可以自定义返回,一般工程应用中的做法,会在方法的返回值上增加一个error返回值,Golang允许每个函数返回多个返回值,增加一个error的作用在于,获取函数返回值的时候,根据error参数进行判断,如果是nil表示没有错误,正常处理,否则处理错误逻辑。这样减少代码出现异常情况

panic 抛出的堆栈信息排查

如果某些情况下,没有捕获异常,程序在运行过程中出现panic,一般都会有一些堆栈信息,我们如何根据这些堆栈信息快速定位并解决呢 ?

一般信息里面都会表明是哪种类似的panic,如是空指针异常还是数组越界,还是xxx;

然后会打印一堆信息出来包括出现异常的代码调用块及其文件位置,需要定位到最后的位置然后反推上去

分析示例如下

 1{"date":"2017-11-22 19:33:20.921","pid":17,"level":"ERROR","file":"recovery.go","line":16,"func":"1","msg":"panic in /Message.MessageService/Proces
 2s: runtime error: invalid memory address or nil pointer dereference
 3github.com.xxx/demo/biz/vendor/github.com.xxx/demo/commons/interceptor.newUnaryServerRecoveryInterceptor.func1.1
 4        /www/jenkins_home/.jenkins/jobs/demo/jobs/demo--biz/workspace/src/github.com.xxx/demo/biz/vendor/github.com.xxx/demo/commons/
 5interceptor/recovery.go:17
 6runtime.call64
 7        /www/jenkins_home/.jenkins/tools/org.jenkinsci.plugins.golang.GolangInstallation/go1.9/go/src/runtime/asm_amd64.s:510
 8runtime.gopanic
 9        /www/jenkins_home/.jenkins/tools/org.jenkinsci.plugins.golang.GolangInstallation/go1.9/go/src/runtime/panic.go:491
10runtime.panicmem
11        /www/jenkins_home/.jenkins/tools/org.jenkinsci.plugins.golang.GolangInstallation/go1.9/go/src/runtime/panic.go:63
12runtime.sigpanic
13        /www/jenkins_home/.jenkins/tools/org.jenkinsci.plugins.golang.GolangInstallation/go1.9/go/src/runtime/signal_unix.go:367
14github.com.xxx/demo/biz/vendor/github.com.xxx/demo/mtrace-middleware-go/grpc.OpenTracingClientInterceptor.func1
15        /www/jenkins_home/.jenkins/jobs/demo/jobs/demo--biz/workspace/src/github.com.xxx/demo/biz/vendor/github.com.xxx/demo/m
16trace-middleware-go/grpc/client.go:49
17github.com.xxx/demo/biz/vendor/github.com/grpc-ecosystem/go-grpc-middleware.ChainUnaryClient.func2.1.1
18        /www/jenkins_home/.jenkins/jobs/demo/jobs/demo--biz/workspace/src/github.com.xxx/demo/biz/vendor/github.com/grpc-ecosystem/go-gr
19pc-middleware/chain.go:90
20github.com.xxx/demo/biz/vendor/github.com/grpc-ecosystem/go-grpc-middleware/retry.UnaryClientInterceptor.func1
21

问题分析

通过报错的堆栈信息,可以看到具体错误是“runtime error: invalid memory address or nil pointer dereference”,也就是空指针异常,然后逐步定位日志,可以发现最终导致出现异常的函数在这个,如下:

1github.com.xxx/demo/biz/vendor/github.com.xxx/demo/mtrace-middleware-go/grpc.OpenTracingClientInterceptor.func1
2
3     /www/jenkins_home/.jenkins/jobs/demo/jobs/demo--biz/workspace/src/github.com.xxx/demo/biz/vendor/github.com.xxx/demo/m
4trace-middleware-go/grpc/client.go:49

一般panic,都会有上述错误日志,然后通过日志,可以追踪到具体函数,然后看到OpenTracingClientInterceptor后,是在client.go的49行,然后开始反推,通过代码可以看到,可能是trace指针为空。然后一步一步看是从哪里开始调用的

最终发现代码如下:

1ucConn, err := grpcclient.NewClientConn(conf.Discovery.UserCenter, newBalancer, time.Second*3, conf.Tracer)
2    if err != nil {
3        logger.Fatalf(nil, "init user center client connection failed: %v", err)
4        return
5    }
6    UserCenterClient = pb.NewUserCenterServiceClient(ucConn)

那么开始排查,conf.Tracer是不是可能为空,在哪里初始化,初始化有没有错,然后发现这个函数是在init里面,然后conf.Tracer确实在main函数里面显示调用的,main函数里面会引用或者间接引用所有包,那么init就一定在main之前执行。

这样的话,init执行的时候,conf.Tracer还没有被赋值,因此就是nil,就会导致panic了

项目工程级别接口

项目中如果能够有一些调试debug接口,有一些pprof性能分析接口,有探测、健康检查接口的话,会给整个项目在线上稳定运行带来很大的作用。 除了pprof性能分析接口属于Golang特有,其他的接口在任何语言都有,这里只是表明在一个工程中,需要有这类型的接口

上下线接口

我们的工程是通过etcd进行服务发现和注册的,同时还提供http服务,那么就需要有个机制来上下线,这样上线过程中,如果服务本身还没有全部启动完成准备就绪,那么就暂时不要在etcd里面注册,不要上线,以免有请求过来,等到就绪后再注册;下线过程中,先从etcd里面移除,这样流量不再导入过来,然后再等待一段时间用来处理还未完成的任务

我们的做法是,start 和 stop 服务的时候,调用API接口,然后再在服务的API接口里面注册和反注册到etcd

 1var OnlineHook = func() error {
 2        return nil
 3    }
 4
 5    var OfflineHook = func() error {
 6        return nil
 7    }
 8
 9
10   // 初始化两个函数,注册和反注册到etcd的函数
11    api.OnlineHook = func() error {
12        return registry.Register(conf.Discovery.RegisterAddress)
13    }
14
15    api.OfflineHook = func() error {
16        return registry.Deregister()
17    }
18
19
20    // 设置在线的函数里面分别调用上述两个函数,用来上下线
21    func SetOnline(isOnline bool) (err error) {
22        if conf.Discovery.RegisterEnabled {
23            if !isServerOnline && isOnline {
24                err = OnlineHook()
25            } else if isServerOnline && !isOnline {
26                err = OfflineHook()
27            }
28        }
29
30        if err != nil {
31            return
32        }
33
34        isServerOnline = isOnline
35        return
36    }
37
38
39    SetOnline 为Http API接口调用的函数

nginx 探测接口,健康检查接口

对于http的服务,一般访问都通过域名访问,nginx配置代理,这样保证服务可以随意扩缩容,但是nginx既然配置了代码,后端节点的情况,就必须要能够有接口可以探测,这样才能保证流量导入到的节点一定的在健康运行中的节点;为此,服务必须要提供健康检测的接口,这样才能方便nginx代理能够实时更新节点。

这个接口如何实现?nginx代理一般通过http code来处理,如果返回code=200,认为节点正常,如果是非200,认为节点异常,如果连续采样多次都返回异常,那么nginx将节点下掉

如提供一个/devops/status 的接口,用来检测,接口对应的具体实现为:

 1func CheckHealth(c *gin.Context) {
 2   // 首先状态码设置为非200,如503
 3    httpStatus := http.StatusServiceUnavailable
 4    // 如果当前服务正常,并服务没有下线,则更新code
 5    if isServerOnline {
 6        httpStatus = http.StatusOK
 7    }
 8
 9    // 否则返回code为503
10    c.IndentedJSON(httpStatus, gin.H{
11        onlineParameter: isServerOnline,
12    })
13}

PProf性能排查接口

 1// PProf
 2    profGroup := debugGroup.Group("/pprof")
 3    profGroup.GET("/", func(c *gin.Context) {
 4        pprof.Index(c.Writer, c.Request)
 5    })
 6    profGroup.GET("/goroutine", gin.WrapH(pprof.Handler("goroutine")))
 7    profGroup.GET("/block", gin.WrapH(pprof.Handler("block")))
 8    profGroup.GET("/heap", gin.WrapH(pprof.Handler("heap")))
 9    profGroup.GET("/threadcreate", gin.WrapH(pprof.Handler("threadcreate")))
10
11    profGroup.GET("/cmdline", func(c *gin.Context) {
12        pprof.Cmdline(c.Writer, c.Request)
13    })
14    profGroup.GET("/profile", func(c *gin.Context) {
15        pprof.Profile(c.Writer, c.Request)
16    })
17    profGroup.GET("/symbol", func(c *gin.Context) {
18        pprof.Symbol(c.Writer, c.Request)
19    })
20    profGroup.GET("/trace", func(c *gin.Context) {
21        pprof.Trace(c.Writer, c.Request)
22    })

debug调试接口

 1// Debug
 2    debugGroup := engine.Group("/debug")
 3    debugGroup.GET("/requests", func(c *gin.Context) {
 4        c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
 5        trace.Render(c.Writer, c.Request, true)
 6    })
 7    debugGroup.GET("/events", func(c *gin.Context) {
 8        c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
 9        trace.RenderEvents(c.Writer, c.Request, true)
10    })

开关【降级】实时调整接口

前面有讲到过,在代码里面需要有开关和降级机制,并讲了实现示例,那么如果需要能够实时改变开关状态,并且实时生效,我们就可以提供一下http的API接口,供运维人员或者开发人员使用。

1// Switch
2    console := engine.Group("/switch")
3    {
4        console.GET("/list", httpsrv.MakeHandler(ListSwitches))
5        console.GET("/status", httpsrv.MakeHandler(CheckSwitchStatus))
6        console.POST("/turnOn", httpsrv.MakeHandler(TurnSwitchOn))
7        console.POST("/turnOff", httpsrv.MakeHandler(TurnSwitchOff))
8    }

go test 单元测试用例

单元测试用例是必须,是自测的一个必要手段,Golang里面单元测试非常简单,import testing 包,然后执行go test,就能够测试某个模块代码

如,在某个user文件夹下有个user包,包文件为user.go,里面有个Func UpdateThemesCounts,如果想要进行test,那么在同级目录下,建立一个user_test.go的文件,包含testing包,编写test用例,然后调用go test即可

一般的规范有:

  • 每个测试函数必须导入testing包
  • 测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头
  • 将测试文件和源码放在相同目录下,并将名字命名为{source_filename}_test.go
  • 通常情况下,将测试文件和源码放在同一个包内。

如下:

 1// user.go
 2func  UpdateThemesCounts(ctx context.Context, themes []int, count int) error {
 3    redisClient := model.GetRedisClusterForTheme(ctx)
 4    key := themeKeyPattern
 5    for _, theme := range themes {
 6        if redisClient == nil {
 7            return errors.New("now redis client")
 8        }
 9
10        total, err := redisClient.HIncrBy(ctx, key, theme, count)
11        if err != nil {
12            logger.Errorf(ctx, "add key:%v for theme:%v count:%v failed:%v", key, theme, count, err)
13            return err
14        } else {
15            logger.Infof(ctx, "now key:%v theme:%v total:%v", key, theme, total)
16        }
17    }
18
19    return nil
20}
21
22
23//user_test.go
24package user
25
26import (
27    "fmt"
28    "testing"
29    "Golang.org/x/net/context"
30)
31
32func TestUpdateThemeCount(t *testing.T) {
33    ctx := context.Background()
34    theme := 1
35    count := 123
36    total, err := UpdateThemeCount(ctx, theme, count)
37    fmt.Printf("update theme:%v counts:%v err:%v \n", theme, total, err)
38}
39
40在此目录下执行 go test即可出结果

测试单个文件 or 测试单个包

通常,一个包里面会有多个方法,多个文件,因此也有多个test用例,假如我们只想测试某一个方法的时候,那么我们需要指定某个文件的某个方案

如下:

 1allen.wu@allen.wudeMacBook-Pro-4:~/Documents/work_allen.wu/goDev/Applications/src/github.com.xxx/avatar/app_server/service/centralhub$tree .
 2.
 3├── msghub.go
 4├── msghub_test.go
 5├── pushhub.go
 6├── rtvhub.go
 7├── rtvhub_test.go
 8├── userhub.go
 9└── userhub_test.go
10
110 directories, 7 files

总共有7个文件,其中有三个test文件,假如我们只想要测试rtvhub.go里面的某个方法,如果直接运行go test,就会测试所有test.go文件了。

因此我们需要在go test 后面再指定我们需要测试的test.go 文件和 它的源文件,如下:

1go test -v msghub.go  msghub_test.go 

测试单个文件下的单个方法

在测试单个文件之下,假如我们单个文件下,有多个方法,我们还想只是测试单个文件下的单个方法,要如何实现?我们需要再在此基础上,用 -run 参数指定具体方法或者使用正则表达式。

假如test文件如下:

 1package centralhub
 2
 3import (
 4    "context"
 5    "testing"
 6)
 7
 8func TestSendTimerInviteToServer(t *testing.T) {
 9    ctx := context.Background()
10
11    err := sendTimerInviteToServer(ctx, 1461410596, 1561445452, 2)
12    if err != nil {
13        t.Errorf("send to server friendship build failed. %v", err)
14    }
15}
16
17func TestSendTimerInvite(t *testing.T) {
18    ctx := context.Background()
19    err := sendTimerInvite(ctx, "test", 1461410596, 1561445452)
20    if err != nil {
21        t.Errorf("send timeinvite to client failed:%v", err)
22    }
23}
1go test -v msghub.go  msghub_test.go -run TestSendTimerInvite
2
3go test -v msghub.go  msghub_test.go -run "SendTimerInvite"

测试所有方法

指定目录即可 go test

测试覆盖度

go test工具给我们提供了测试覆盖度的参数,

go test -v -cover

go test -cover -coverprofile=cover.out -covermode=count

go tool cover -func=cover.out

goalng GC 、编译运行

服务端开发者如果在mac上开发,那么Golang工程的代码可以直接在mac上编译运行,然后如果需要部署在Linux系统的时候,在编译参数里面指定GOOS即可,这样可以本地调试ok后再部署到Linux服务器。

如果要部署到Linux服务,编译参数的指定为

1ldflags="
2  -X ${repo}/version.version=${version}
3  -X ${repo}/version.branch=${branch}
4  -X ${repo}/version.goVersion=${go_version}
5  -X ${repo}/version.buildTime=${build_time}
6  -X ${repo}/version.buildUser=${build_user}
7"
8
9CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "${ldflags}" -o $binary_dir/$binary_name ${repo}/

对于GC,我们要收集起来,记录到日志文件中,这样方便后续排查和定位,启动的时候指定一下即可执行gc,收集gc日志可以重定向

1  export GIN_MODE=release
2    GODEBUG=gctrace=1 $SERVER_ENTRY 1>/dev/null 2>$LOGDIR/gc.log.`date "+%Y%m%d%H%M%S"` &
3
4Golang包管理 目录代码管理

Golang包管理 目录代码管理

目录代码管理

整个项目包括两大类,一个是自己编写的代码模块,一个是依赖的代码,依赖包需要有进行包管理,自己的编写的代码工程需要有一个合适的目录进行管理 main.go :入口 doc : 文档 conf : 配置相关 ops : 运维操作相关【http接口】 api : API接口【http交互接口】 daemon : 后台daemon相关 model : model模块,操作底层资源 service : model的service grpcclient : rpc client registry : etcd 注册 processor : 异步kafka消费

 1.
 2├── README.md
 3├── api
 4├── conf
 5├── daemon
 6├── dist
 7├── doc
 8├── grpcclient
 9├── main.go
10├── misc
11├── model
12├── ops
13├── processor
14├── registry
15├── service
16├── tools
17├── vendor
18└── version

包管理

go允许import不同代码库的代码,例如github.com, golang.org等等;对于需要import的代码,可以使用 go get 命令取下来放到GOPATH对应的目录中去。

对于go来说,其实并不care你的代码是内部还是外部的,总之都在GOPATH里,任何import包的路径都是从GOPATH开始的;唯一的区别,就是内部依赖的包是开发者自己写的,外部依赖的包是go get下来的。

依赖GOPATH来解决go import有个很严重的问题:如果项目依赖的包做了修改,或者干脆删掉了,会影响到其他现有的项目。为了解决这个问题,go在1.5版本引入了vendor属性(默认关闭,需要设置go环境变量GO15VENDOREXPERIMENT=1),并在1.6版本之后都默认开启了vendor属性。 这样的话,所有的依赖包都在项目工程的vendor中了,每个项目都有各自的vendor,互不影响;但是vendor里面的包没有版本信息,不方便进行版本管理。

目前市场上常用的包管理工具主要有godep、glide、dep

godep

godep的使用者众多,如docker,kubernetes, coreos等go项目很多都是使用godep来管理其依赖,当然原因可能是早期也没的工具可选,早期我们也是使用godep进行包管理。

使用比较简单,godep save;godep restore;godep update;

但是后面随着我们使用和项目的进一步加强,我们发现godep有诸多痛点,目前已经逐步开始弃用godep,新项目都开始采用dep进行管理了。

godep的痛点:

  • godep如果遇到依赖项目里有vendor的时候就可能会导致编译不过,vendor下再嵌套vendor,就会导致编译的时候出现版本不一致的错误,会提示某个方法接口不对,全部放在当前项目的vendor下
  • godep锁定版本太麻烦了,在项目进一步发展过程中,我们依赖的项目(包)可能是早期的,后面由于升级更新,某些API接口可能有变;但是我们项目如果已经上线稳定运行,我们不想用新版,那么就需要锁定某个特定版本。但是这个对于godep而言,操作着实不方便。
  • godep的时候,经常会有一些包需要特定版本,然后包依赖编译不过,尤其是在多人协作的时候,本地gopath和vendor下的版本不一样,然后本地gopath和别人的gopath的版本不一样,导致编译会遇到各种依赖导致的问题

glide

glide也是在vendor之后出来的。glide的依赖包信息在glide.yaml和glide.lock中,前者记录了所有依赖的包,后者记录了依赖包的版本信息

glide create # 创建glide工程,生成glide.yaml glide install # 生成glide.lock,并拷贝依赖包 glide update # 更新依赖包信息,更新glide.lock

因为glide官方说我们不更新功能了,只bugfix,请大家开始使用dep吧,所以鉴于此,我们在选择中就放弃了。同时,glide如果遇到依赖项目里有vendor的时候就直接跪了,dep的话,就会滤掉,不会再vendor下出现嵌套的,全部放在当前项目的vendor下

dep

golang官方出品,dep最近的版本已经做好了从其他依赖工具的vendor迁移过来的功能,功能很强大,是我们目前的最佳选择。不过目前还没有release1.0 ,但是已经可以用在生成环境中,对于新项目,我建议采用dep进行管理,不会有历史问题,而且当新项目上线的时候,dep也会进一步优化并且可能先于你的项目上线。

dep默认从github上拉取最新代码,如果想优先使用本地gopath,那么3.x版本的dep需要显式参数注明,如下

1dep init -gopath -v

总结

  • godep是最初使用最多的,能够满足大部分需求,也比较稳定,但是有一些不太好的体验;
  • glide 有版本管理,相对强大,但是官方表示不再进行开发;
  • dep是官方出品,目前没有release,功能同样强大,是目前最佳选择;

看官方的对比

Golang容易出现的问题

包引用缺少导致panic

go vendor 缺失导致import多次导致panic

本工程下没有vendor目录,然而,引入了这个包“github.com.xxx/demo/biz/model/impl/hash”, 这个biz包里面包含了vendor目录。

这样,编译此工程的时候,会导致一部分import是从oracle下的vendor,另一部分是从gopath,这样就会出现一个包被两种不同方式import,导致出现重复注册而panic

并发 导致 panic

fatal error: concurrent map read and map write

并发编程中最容易出现资源竞争,以前玩C++的时候,资源出现竞争只会导致数据异常,不会导致程序异常panic,Golang里面会直接抛错,这个是比较好的做法,因为异常数据最终导致用户的数据异常,影响很大,甚至无法恢复,直接抛错后交给开发者去修复代码bug,一般在测试过程中或者代码review过程中就能够发现并发问题。

并发的处理方案有二:

  1. 通过chann 串行处理
  2. 通过加锁控制

相互依赖引用导致编译不过

Golang不允许包直接相互import,会导致编译不过。但是有个项目里面,A同学负责A模块,B同学负责B模块,由于产品需求导致,A模块要调用B模块中提供的方法,B模块要调用A模块中提供的方法,这样就导致了相互引用了

我们的解决方案是: 将其中一个相互引用的模块中的方法提炼出来,独立为另外一个模块,也就是另外一个包,这样就不至于相互引用

Golang json类型转换异常

Golang进行json转换的时候,常用做法是一个定义struct,成员变量使用tag标签,然后通过自带的json包进行处理,容易出现的问题主要有:

  1. 成员变量的首字母没有大写,导致json后生成不了对应字段
  2. json string的类型不对,导致json Unmarshal 的时候抛错

Golang 总结

golang使用一年多以来,个人认为golang有如下优点:

  • 学习入门快;让开发者开发更为简洁
  • 不用关心内存分配和释放,gc会帮我们处理;
  • 效率性能高;
  • 不用自己去实现一些基础数据结构,官方或者开源库可以直接import引用;
  • struct 和 interface 可以实现类、继承等面向对象的操作模式;
  • 初始化和赋值变量简洁;
  • 并发编程goroutine非常容易,结合chann可以很好的实现;
  • Context能够自我控制开始、停止,传递上下文信息

版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。

Golang语言社区

ID:Golangweb

www.bytedancing.com

游戏服务器架构丨分布式技术丨大数据丨游戏算法学习

原文发布于微信公众号 - Golang语言社区(Golangweb)

原文发表时间:2018-09-01

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏向治洪

xmpp即时通讯四

     TLS协商(5节)后,如果需要SASL协商(6节)与资源绑定(7节),XML节可通过流来发送。定义了三种XML节用于 'jabber:client'与...

1975
来自专栏码洞

求不更学不动之Redis5.0新特性Stream尝鲜

Redis5.0最近被作者突然放出来了,增加了很多新的特色功能。而Redis5.0最大的新特性就是多出了一个数据结构Stream,它是一个新的强大的支持多播的可...

1646
来自专栏技术小黑屋

一个Android代码JIT友好度检测工具

利用周末的时间,写了一个检测Android代码JIT友好度的工具,取个名字为DroidJitChecker。希望可以帮助大家快速发现有坏味道的代码,并且及时修正...

1084
来自专栏信安之路

通过POC来学习漏洞的原理

本文介绍的是 easyFTPServer 1.7.0.2 ‘Http’ remote Buffer Overflow 的漏洞执行流程,通过已知的 POC 来推断...

1460
来自专栏枕边书

请求合并哪家强

工作中,我们常见的请求模型都是请求-应答式,即一次请求中,服务给请求分配一个独立的线程,一块独立的内存空间,所有的操作都是独立的,包括资源和系统运算。我们也知道...

892
来自专栏互联网大杂烩

拜占庭容错机制

Client会发送一系列请求给各个replicas节点来执行相应的操作,BFT算法保证所有正常的replicas节点执行相同序列的操作。因为所有的replica...

842
来自专栏MySQL

深入理解JAVA中的NIO

传统的 IO 流还是有很多缺陷的,尤其它的阻塞性加上磁盘读写本来就慢,会导致 CPU 使用效率大大降低。

1685
来自专栏有趣的Python

12- Flask构建弹幕微电影网站-后台逻辑(四)

已上线演示地址: http://movie.mtianyan.cn 项目源码地址:https://github.com/mtianyan/movie_proj...

2826
来自专栏Jackson0714

不惧面试:HTTP协议(3) - Cookie

27410
来自专栏linux驱动个人学习

SMP多核启动

在 Linux系统中,对于多核的ARM芯片而言,在Biotron代码中,每个CPU都会识别自身ID,如果ID是0,则引导Bootloader和 Linux内核执...

1925

扫码关注云+社区