文章目录
学习资料
go官方文档:https://golang.google.cn/doc/
go语言中文网:https://studygolang.com/
《Effective Go》:https://golang.google.cn/doc/effective_go
《The Go Programming Language》
英文版:https://github.com/adonovan/gopl.io/
中文版:https://github.com/gopl-zh/gopl-zh.github.com
《go语言原本》:https://golang.design/under-the-hood/
《go语言设计与实现》:https://draveness.me/golang/
《Go语言101》:https://gfw.go101.org/article/101.html
《go语言问题集》:https://www.bookstack.cn/read/qcrao-Go-Questions/README.md
工程项目结构
参考该文章
https://github.com/golang-standards/project-layout/blob/master/README_zh.md
/cmd
本项目的主干。
每个应用程序的目录名应该与想要的可执行文件的名称相匹配(例如,/cmd/myapp)。
不要在这个目录中放置太多代码,通常有一个小的 main 函数,从 /internal 和 /pkg 目录导入和调用代码。
/internal
私有应用程序和库代码,不希望其他人在其应用程序或库中导入的代码。
这个布局模式是由 Go 编译器本身执行的。Go语言1.4版本增加了 Internal packages 特征用于控制包的导入,即internal package只能被特定的包导入。内部包的规范约定:导出路径包含internal关键字的包,只允许internal的父级目录及父级目录的子包导入,其它包无法导入。
并不局限于顶级 internal 目录,在项目树的任何级别上都可以有多个内部目录。
应用程序共享的代码可以放在 /internal/pkg 目录下(例如 /internal/pkg/myprivlib)。
实际应用程序代码可以放在 /internal/app 目录下(例如 /internal/app/myapp),其下还可以分出
../model、../dao、../service、../dto 等子目录。
/pkg
外部应用程序可以使用的库代码。
其他项目会导入这些库,希望它们能正常工作,所以在这里放东西之前要三思
/internla/pkg 一般用于项目内的跨多个模块的公共共享代码,但其作用域仅在单个项目工程内。
/api
OpenAPI 规范,JSON 模式文件,协议定义文件。
如果有多版本可以使用/api/v1/myapi
/config
配置文件模板或默认配置。
/web
特定于 Web 应用程序的组件: 静态 Web 资产、服务器端模板和 SPAs。
/scripts
执行各种构建、安装、分析等操作的脚本。
/test
额外的外部测试应用程序和测试数据。
对于较大的项目,有一个数据子目录是有意义的。例如/test/data 或 /test/testdata
不应该包含:/src 目录,容易与 GOPATH 的 src 目录产生歧义
包管理
Golang 官方包管理工具的发展历史
- 在 1.5 版本之前,所有的依赖包都是存放在 GOPATH 下,没有版本控制。这种方式的最大的弊端就是无法实现包的多版本控制,比如项目 A 和项目 B 依赖于不同版本的 package,如果 package 没有做到完全的向前兼容,往往会导致一些问题。
- 1.5 版本推出了 vendor 机制。所谓 vendor 机制,就是每个项目的根目录下可以有一个 vendor 目录,里面存放了该项目的依赖的 package。
go build的时候会先去 vendor 目录查找依赖,如果没有找到会再去 GOPATH 目录下查找。 - 1.9 版本推出了实验性质的包管理工具 dep。
- 1.11 版本推出 modules 机制,简称 mod。
go mod 的使用
-
环境变量 GO111MODULE
-
GO111MODULE=off: 不使用 modules 功能,查找vendor和GOPATH目录
-
GO111MODULE=on: 使用 modules 功能,不会去 GOPATH 下面查找依赖包。
-
GO111MODULE=auto: Golang 自己检测是不是使用 modules 功能,如果当前目录不在$GOPATH 并且当前目录(或者父目录)下有go.mod文件,则使用 GO111MODULE, 否则仍旧使用 GOPATH mode
set GO111MODULE=on //windows export GO111MODULE=on //linux
modules 在 Go 1.13 的版本后是默认开启的
-
-
初始化
go mod init test-demo go run main.go执行 init 暂时还没有将所有的依赖管理起来。需要将程序 run 起来(比如执行 go run/test),或者 build(执行命令 go build)的时候,才会触发依赖的解析。
同时项目目录下多了一个 go.sum 用来记录每个 package 的版本和哈希值。
这些 package 并不是直接存储到 GOPATH/src,而是存储到 GOPATH/pkg/mod 下面,不同版本并存的方式。
-
go.mod
module test-demo go 1.16 require ( github.com/astaxie/beego v1.12.0 // indirect github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 // indirect ) replace gopkg.in/yaml.v2 v2.2.1 => github.com/yaml v2.2.1go.mod 文件正常情况会包含 module 和 require 模块,除此之外还可以包含 replace 和 exclude 模块。
有indirect注释的代表间接依赖,没有的代表直接依赖,这里是版本号+时间戳+hash
-
依赖的升级和降级
//查看当前项目依赖的所有包 go list -m -u all //升级(降级)某个 package 则只需要 go get 即可,比如: go get package@version -
go proxy
有些 Golang 的 package 在国内是无法直接 go get 的,可以通过设置 http_proxy/https_proxy 来解决。
GoProxy 相当于官方提供了一种 proxy 的方式让用户来进行包下载。
要使用 GoProxy 只需要设置环境变量
GOPROXY即可。目前公开的 GOPROXY 有:- http://goproxy.io
- http://goproxy.cn :由七牛云提供
- https://proxy.golang.org :Go 1.13 版本后 GOPROXY 的默认值,这个对于国内的开发者是无法直接使用的。所以一定要把 GOPROXY 手动改掉。
go env -w GOPROXY=https://goproxy.cn,direct -
go mod 常用命令
go mod init:初始化modules go test 执行一下,自动导包 go mod download:下载modules到本地cache go mod edit:编辑go.mod文件,选项有-json、-require和-exclude,可以使用帮助go help mod edit go mod graph:以文本模式打印模块需求图 go mod tidy:检查,删除错误或者不使用的modules,下载没download的package go mod verify:验证依赖是否正确 go mod why:查找依赖 go list -m 主模块的打印路径 go list -m -f={{.Dir}} print主模块的根目录 go list -m all 查看当前的依赖和版本信息
日志处理
go的log包
-
控制台输出日志
package main import ( "errors" "fmt" "log" ) func division(x float32, y float32) (float32, error) { if y == 0 { return 0, errors.New("can't divide by zero") } return x / y, nil } func main() { var x float32 = 11 var y float32 res, err := division(x, y) if err != nil { log.Print(err) } fmt.Println(res) } -
文件输出日志
func main() { file, err := os.OpenFile("info.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) if err != nil { log.Fatal(err) } defer file.Close() log.SetOutput(file) var x float32 = 11 var y float32 res, exception := division(x, y) if exception != nil { log.Print(exception) } fmt.Println(res) }
glog 日志库
github:https://github.com/golang/glog
logrus日志库
github:https://github.com/sirupsen/logrus
日志的记录原则
- 错误要被日志记录
- 程序中处理错误要保证100%的完整性,要么往上层抛,要么处理,如果要吞掉error,应该对value的值负责任
- 错误只记录一次,如果当前对错误进行了处理并记录了日志,那么抛到上层之后不再记录当前值
错误处理
go的errors包和error接口
package errors
func New(text string) error {
return &errorString{text}
}
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
go的 errors 包定义了 New()函数,其返回值是内部 errorString 对象的指针,这样可以避免在进行error类型断言时误将字符串值相等但是类型不同的两个error判断为相等
errorString 对象实现了 error 接口,重写了 Error() 方法,返回值是其内部定义的string类型的变量 s
package builtin
type error interface {
Error() string
}
func panic(v interface{})
func recover() interface{}
go支持多参数返回,所以可以在函数的返回值中带上实现了 error 接口的对象,交由调用者处理,让调用者知道什么时候出了问题。
对于程序运行时的异常,go保留了 panic 机制。panic 意味着代码不能继续运行,程序挂了,不能假设调用者来解决 panic
关于panic
-
不要在业务代码中主动制造panic,只能用来表示那些不可恢复的程序错误,比如索引越界、栈溢出、不可恢复的环境问题等。
-
对于初始化的代码,比如强依赖配置类的代码,可以panic。以便我们看到错误信息,及时做出修改
-
野生的goroutine是recover不住的,应该在代码中避免。
func main() { defer func() { if err := recover(); err != nil{ fmt.Println(err) } }() go func() { fmt.Println("Hello world!") panic("我来啦!") }() }可以在项目的基础库里自定义一个包,开goroutine时默认自动 recover
func Go(x func()){ go func() { defer func() { if err := recover(); err != nil { fmt.Println(err) } }() x() }() } func main(){ Go(func(){ fmt.Println("Hello world!") panic("我来啦!") }) }
error的定义方式
参考下列文章
https://blog.golang.org/error-handling-and-go
https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully
https://ethancai.github.io/2017/12/29/Error-Handling-in-Go/
-
errors.New()
用特定的值来表示 error,这样在处理时需要进行字符串判等,在两个包之间创建了依赖,应该尽量避免这样使用
var MyError = errors.New("xxxx") -
fmt.Errorf()
可以通过 fmt.Errorf() 扩展 error,为 error 携带更多的上下文
func readfile(path string) error { err := openfile(path) if err != nil { return fmt.Errorf("cannot open file: %v", err) } //... } func main() { err := readfile(".bashrc") //这种断言方式是不应该出现的 if strings.Contains(error.Error(), "not found") { // handle error } }error interface 的 Error() 的输出是给人看的,不是给机器看的。我们通常会把
Error方法返回的字符串打印到日志中,或者显示在控制台上。永远不要通过判断 Error 方法返回的字符串是否包含特定字符串,来决定错误处理的方式。
-
error type
与错误值相比,错误类型的一大改进是它们能够包装底层错误以提供更多上下文
type MyError stuct{ Code int Msg string File string Line string } func (e *MyError) Error() string { return fmt.Sprintf("%d, %s, %s, %s,", e.Code, e.Msg, e.File, e.Line) }自定义的错误类型因为要进行类型断言,所以需要将其设置为public的。
这种错误定义方式也是有包级依赖关系的
-
不透明 error
不透明错误处理的全部功能,只需返回错误而不假设其内容,就是不返回错误的具体内容。
type temporary interface { Temporary() bool // IsTemporary returns true if err is temporary. } //对外暴露的调用方法 func IsTemporary(err error) bool { te, ok := err.(temporary) return ok && te.Temporary() }定义一个包级私有的接口 temporary,接口中定义一个方法 Temporary。对外暴露一个包级别的公开方法 IsTemporary,在这个方法中断言传入的error类型是否实现了temporary 接口,然后确定是否调用业务方法。
这种实现方式的好处在于,不需要知道具体的错误类型,也就不需要引用定义了错误类型的第三方
package。对于底层代码,如果想更换一个实现更好的
error,也不用担心影响上层代码逻辑。对于上层代码的开发者,只需要关注
error是否实现了特定行为,不用担心引用的第三方package升级后,程序逻辑失败。这是最灵活的错误处理策略,因为它要求代码和调用者之间的耦合最少。
error的断言方法
-
字符串判等
//这种直接断言字符串的做法是不应该的 err := test() if err.Error() == "xxx" { fmt.Printf("error: %s", err) } //应该按照error的类型去断言 if err, ok := err.(*MyError); ok { fmt.Printf("%d, %s", err.Code, err.Msg) } if err == io.EOF { fmt.Printf("error:%s", err) } -
switch断言
err := test() swith err := err.(type){ case nil: //successed nothing to do case *MyError: fmt.Println("error:", err.Msg) default: //unknown error } -
errors.Is() 将错误与值进行比较
https://blog.golang.org/go1.13-errors
当多层调用返回的错误被一次次地包装起来,我们在调用链上游拿到的错误判断是否是底层的某个错误。
errors.Is()递归调用Unwrap()并判断每一层的 err 是否相等,如果有任何一层 err 和传入的目标错误相等,则返回 true。if errors.Is(err, io.EOF){ //... } -
errors.As() 测试错误是否为特定类型。
这个和上面的
errors.Is大体上是一样的,区别在于Is是严格判断相等,即两个error是否相等。
而As则是判断类型是否相同,并提取第一个符合目标类型的错误,用来统一处理某一类错误。// Similar to: // if e, ok := err.(*QueryError); ok { … } var e *QueryError // Note: *QueryError is the type of the error. if errors.As(err, &e) { // err is a *QueryError, and e is set to the error's value }
error的处理方式
在代码中不能对于error做任何正确的假定,就是所有的error都需要判断处理,但是我们应该做到优雅的错误处理,增强程序的可读性
-
缩进,把业务逻辑不要写在 if 里面
f, err := os.Open(path) if err != nil { // handle error } // do stuff不要像下面这样写
f, err := os.Open(path) if err == nil { // do stuff } // handle error -
对于中间层代码,不需要显式处理错误时,不要再判断 if err != nil
func AuthenticateRequest(r *Request) error { err := authenticate(r.User) if err != nil { return err // No such file or directory } return nil }可以改写成以下这样
func AuthenticateRequest(r *Request) error { return authenticate(r.User) } -
如果同一个方法中重复调用同一个函数或者方法,将重复的逻辑进行了封装,然后把 error 暂存,就只需要在最后判断一下 error 就行了
示例原文:https://blog.golang.org/errors-are-values
重复的逻辑
var fd io.Writer _, err = fd.Write(p0[a:b]) if err != nil { return err } _, err = fd.Write(p1[c:d]) if err != nil { return err } _, err = fd.Write(p2[e:f]) if err != nil { return err } // and so on优雅的实现方式
type errWriter struct { w io.Writer err error } func (ew *errWriter) write(buf []byte) { if ew.err != nil { return } _, ew.err = ew.w.Write(buf) } // 使用时 ew := &errWriter{w: fd} ew.write(p0[a:b]) ew.write(p1[c:d]) ew.write(p2[e:f]) // and so on if ew.err != nil { return ew.err } -
对于堆栈信息的携带
github:https://github.com/pkg/errors
errors.wrap
使用 pkg/errors 的 Wrap 或者 Wrapf 函数对堆栈信息进行封装
package main import ( "errors" "fmt" xerrors "github.com/pkg/errors" ) type ErrPath struct { msg string } func (p *ErrPath) Error() string { return p.msg } func main() { err := test2() // var e *ErrPath e:= &ErrPath{} if errors.As(err, &e) { fmt.Println("err is ErrPath") } fmt.Printf("main: %+v\n", err) } //从这一层开始,每一次向上抛error时,堆栈信息会被包装一次 func test0() error { return xerrors.Wrap(&ErrPath{msg: "path not exists"}, "test0 failed") } func test1() error { return test0() } func test2() error { return test1() }在需要打印堆栈信息时,使用谓词
%+v输出堆栈信息fmt.Printf("main: %+v\n", err)使用 errors.Cause()可以获取 root error
switch err := errors.Cause(err).(type) { case *MyError: // handle specifically default: // unknown error }此外pkg/errors包还提供
withStack()函数包装堆栈信息,WithMessage()函数包装上下文信息go1.13 后还提供了
errors.Unwrap()对包装的error上下文和堆栈信息进行解包,可以根据需要对这些方法进行结合使用 -
关于error处理的一些规则总结
- 在应用代码中使用errors.New 或者 errors.Errorf 返回错误
- 如果只调用项目中其他包内的函数,没有更多业务逻辑,通常直接返回
- 直接返回错误,而不是产生错误的地方到处打日志
- 根据场景进行判断是否需要将其他库的原始错误吞掉,例如可以把 dao层的数据库相关错误吞掉,返回业务错误码,避免后续更换库时需要去修改上层代码
- 和其他库(项目公共库或第三方库,类如与数据库操作的库),使用errors.warp 或者errors.warpf保存堆栈信息
- 在我们项目中定义的基础库中不要使用 errors.warp,避免堆栈信息重复
- 在程序的顶部或者是工作的goroutine顶部(请求入口),使用 %+v 把堆栈详情记录
- 使用 errors.Cause() 获取 root error,再进行和 sentinel error 判定
- 对于业务错误,在一个统一的地方创建一个错误字典,错误字典里面应该包含错误的code,并且在日志中作为独立字段打印,方便做业务告警的判断,错误应该有清晰的错误文档
本文详细介绍了Go语言项目的标准目录结构,包括各部分的功能和用途,以及Go语言中的包管理和日志处理方法。此外,还深入探讨了错误处理的最佳实践,包括错误的定义、断言和处理方式。
782

被折叠的 条评论
为什么被折叠?



