duyong mac 5 miesięcy temu
rodzic
commit
9e32082a30
9 zmienionych plików z 687 dodań i 0 usunięć
  1. 25 0
      .gitignore
  2. 81 0
      errors/coder.go
  3. 63 0
      errors/errcode.go
  4. 80 0
      errors/errors.go
  5. 168 0
      errors/format.go
  6. 174 0
      errors/stack.go
  7. 81 0
      examples/errors/main.go
  8. 5 0
      go.mod
  9. 10 0
      go.sum

+ 25 - 0
.gitignore

@@ -0,0 +1,25 @@
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, build with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+.idea/
+.fleet/
+.DS_Store
+**/bin
+**/logs
+**/docs
+**/mbtest*
+**/.idea
+
+/vendor/
+/wb
+/logs/

+ 81 - 0
errors/coder.go

@@ -0,0 +1,81 @@
+package errors
+
+import (
+	"fmt"
+	"sync"
+)
+
+type Coder interface {
+	// HTTPStatus 返回http状态码
+	HTTPStatus() int
+	// String 展示给外部用户的错误信息
+	String() string
+	// Reference 展示给用户解决错误需要参开的引用文档
+	Reference() string
+	// Code 错误码
+	Code() int
+}
+
+// 错误码映射
+var codes = map[int]Coder{}
+var codeMux = &sync.Mutex{}
+
+// Register 注册错误码,存在会被覆盖
+func Register(coder Coder) {
+	if coder.Code() == 999999 {
+		panic("code `999999` is reserved by system as unknownCode error code")
+	}
+	codeMux.Lock()
+	defer codeMux.Unlock()
+
+	codes[coder.Code()] = coder
+}
+
+// MustRegister 注册错误码,已注册报错
+func MustRegister(coder Coder) {
+	if coder.Code() == 999999 {
+		panic("code `999999` is reserved by system as unknownCode error code")
+	}
+
+	codeMux.Lock()
+	defer codeMux.Unlock()
+
+	if _, ok := codes[coder.Code()]; ok {
+		panic(fmt.Sprintf("code: %d already exist", coder.Code()))
+	}
+
+	codes[coder.Code()] = coder
+}
+
+// ParseCoder 解析错误码
+// 将错误解析为 *withCode 类型。如果原生错误不是本错误包生成,返回未知错误
+func ParseCoder(err error) Coder {
+	if err == nil {
+		return nil
+	}
+
+	if v, ok := err.(*withCode); ok {
+		if coder, ok := codes[v.code]; ok {
+			return coder
+		}
+	}
+
+	return unknownCoder
+}
+
+// IsCode 判断错误码是否在当前错误链中
+func IsCode(err error, code int) bool {
+	if v, ok := err.(*withCode); ok {
+		if v.code == code {
+			return true
+		}
+
+		if v.cause != nil {
+			return IsCode(v.cause, code)
+		}
+
+		return false
+	}
+
+	return false
+}

+ 63 - 0
errors/errcode.go

@@ -0,0 +1,63 @@
+package errors
+
+import "net/http"
+import "github.com/novalagung/gubrak"
+
+var (
+	unknownCoder ErrCode = ErrCode{1, http.StatusInternalServerError, "An internal server error occurred", ""}
+)
+
+type ErrCode struct {
+	// 业务错误码
+	C int
+
+	// http码
+	HTTP int
+
+	// 对外展示错误信息
+	Ext string
+
+	// 引用
+	Ref string
+}
+
+func (coder ErrCode) Code() int {
+	return coder.C
+}
+
+func (coder ErrCode) String() string {
+	return coder.Ext
+}
+
+func (coder ErrCode) Reference() string {
+	return coder.Ref
+}
+
+func (coder ErrCode) HTTPStatus() int {
+	if coder.HTTP == 0 {
+		return http.StatusInternalServerError
+	}
+
+	return coder.HTTP
+}
+
+func RegisterCode(code int, httpStatus int, message string, refs ...string) {
+	found, _ := gubrak.Includes([]int{200, 400, 401, 403, 404, 500}, httpStatus)
+	if !found {
+		panic("http code not in `200, 400, 401, 403, 404, 500`")
+	}
+
+	var reference string
+	if len(refs) > 0 {
+		reference = refs[0]
+	}
+
+	coder := &ErrCode{
+		C:    code,
+		HTTP: httpStatus,
+		Ext:  message,
+		Ref:  reference,
+	}
+
+	MustRegister(coder)
+}

+ 80 - 0
errors/errors.go

@@ -0,0 +1,80 @@
+package errors
+
+import "fmt"
+
+type withCode struct {
+	err   error
+	code  int
+	cause error
+	*stack
+}
+
+func WithCode(code int, format string, args ...interface{}) error {
+	return &withCode{
+		err:   fmt.Errorf(format, args...),
+		code:  code,
+		stack: callers(),
+	}
+}
+
+func Wrap(err error, message string) error {
+	if err == nil {
+		return nil
+	}
+	if e, ok := err.(*withCode); ok {
+		return &withCode{
+			err:   fmt.Errorf(message),
+			code:  e.code,
+			cause: err,
+			stack: callers(),
+		}
+	}
+
+	return &withCode{
+		err:   fmt.Errorf(message),
+		code:  unknownCoder.Code(),
+		cause: err,
+		stack: callers(),
+	}
+}
+
+func WrapC(err error, code int, format string, args ...interface{}) error {
+	if err == nil {
+		return nil
+	}
+
+	return &withCode{
+		err:   fmt.Errorf(format, args...),
+		code:  code,
+		cause: err,
+		stack: callers(),
+	}
+}
+
+func (w *withCode) Error() string { return fmt.Sprintf("%v", w) }
+
+// Cause 返回根因
+func (w *withCode) Cause() error { return w.cause }
+
+// Unwrap 返回根因
+func (w *withCode) Unwrap() error { return w.cause }
+
+func Cause(err error) error {
+	type causer interface {
+		Cause() error
+	}
+
+	for err != nil {
+		cause, ok := err.(causer)
+		if !ok {
+			break
+		}
+
+		if cause.Cause() == nil {
+			break
+		}
+
+		err = cause.Cause()
+	}
+	return err
+}

+ 168 - 0
errors/format.go

@@ -0,0 +1,168 @@
+package errors
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"strings"
+)
+
+type formatInfo struct {
+	code    int
+	message string
+	err     string
+	stack   *stack
+}
+
+func (w *withCode) Format(state fmt.State, verb rune) {
+	switch verb {
+	case 'v':
+		str := bytes.NewBuffer([]byte{})
+		jsonData := []map[string]interface{}{}
+
+		var (
+			flagDetail bool
+			flagTrace  bool
+			modeJSON   bool
+		)
+
+		if state.Flag('#') {
+			modeJSON = true
+		}
+
+		if state.Flag('-') {
+			flagDetail = true
+		}
+		if state.Flag('+') {
+			flagTrace = true
+		}
+
+		sep := ""
+		errs := list(w)
+		length := len(errs)
+		for k, e := range errs {
+			finfo := buildFormatInfo(e)
+			jsonData, str = format(length-k-1, jsonData, str, finfo, sep, flagDetail, flagTrace, modeJSON)
+			sep = "; "
+
+			if !flagTrace {
+				break
+			}
+
+			if !flagDetail && !flagTrace && !modeJSON {
+				break
+			}
+		}
+		if modeJSON {
+			var byts []byte
+			byts, _ = json.Marshal(jsonData)
+
+			str.Write(byts)
+		}
+
+		fmt.Fprintf(state, "%s", strings.Trim(str.String(), "\r\n\t"))
+	default:
+		finfo := buildFormatInfo(w)
+		// Externally-safe error message
+		fmt.Fprintf(state, finfo.message)
+	}
+}
+
+func format(k int, jsonData []map[string]interface{}, str *bytes.Buffer, finfo *formatInfo,
+	sep string, flagDetail, flagTrace, modeJSON bool) ([]map[string]interface{}, *bytes.Buffer) {
+	if modeJSON {
+		data := map[string]interface{}{}
+		if flagDetail || flagTrace {
+			data = map[string]interface{}{
+				"message": finfo.message,
+				"code":    finfo.code,
+				"error":   finfo.err,
+			}
+
+			caller := fmt.Sprintf("#%d", k)
+			if finfo.stack != nil {
+				f := Frame((*finfo.stack)[0])
+				caller = fmt.Sprintf("%s %s:%d (%s)",
+					caller,
+					f.file(),
+					f.line(),
+					f.name(),
+				)
+			}
+			data["caller"] = caller
+		} else {
+			data["error"] = finfo.message
+		}
+		jsonData = append(jsonData, data)
+	} else {
+		if flagDetail || flagTrace {
+			if finfo.stack != nil {
+				f := Frame((*finfo.stack)[0])
+				fmt.Fprintf(str, "%s%s - #%d [%s:%d (%s)] (%d) %s",
+					sep,
+					finfo.err,
+					k,
+					f.file(),
+					f.line(),
+					f.name(),
+					finfo.code,
+					finfo.message,
+				)
+			} else {
+				fmt.Fprintf(str, "%s%s - #%d %s", sep, finfo.err, k, finfo.message)
+			}
+
+		} else {
+			fmt.Fprintf(str, finfo.message)
+		}
+	}
+
+	return jsonData, str
+}
+
+func list(e error) []error {
+	var ret []error
+
+	if e != nil {
+		if w, ok := e.(interface{ Unwrap() error }); ok {
+			ret = append(ret, e)
+			ret = append(ret, list(w.Unwrap())...)
+		} else {
+			ret = append(ret, e)
+		}
+	}
+
+	return ret
+}
+
+func buildFormatInfo(e error) *formatInfo {
+	var finfo *formatInfo
+
+	err, ok := e.(*withCode)
+	if !ok {
+		return &formatInfo{
+			code:    unknownCoder.Code(),
+			message: err.Error(),
+			err:     err.Error(),
+		}
+	}
+
+	coder, ok := codes[err.code]
+	if !ok {
+		coder = unknownCoder
+	}
+
+	extMsg := coder.String()
+	if extMsg == "" {
+		extMsg = err.err.Error()
+	}
+
+	finfo = &formatInfo{
+		code:    coder.Code(),
+		message: extMsg,
+		err:     err.err.Error(),
+		stack:   err.stack,
+	}
+
+	return finfo
+}

+ 174 - 0
errors/stack.go

@@ -0,0 +1,174 @@
+package errors
+
+import (
+	"fmt"
+	"io"
+	"path"
+	"runtime"
+	"strconv"
+	"strings"
+)
+
+// tip: 计算机中关于程序计数器,不要理解成是程序的执行次数,而是指向某个位置的下标,或者说是一个地址。
+
+// Frame 调用栈程序计数器,用来表示栈指针。
+type Frame uintptr
+
+// pc 返回程序计数器。我们会是用栈指针来获取栈帧对应函数的文件名 行号等信息
+// runtime.Caller 可以返回函数栈某一层的程序计数器,0表示函数本身,1表示调用者
+// runtime.Callers 可以返回某个调用链的所有程序计数器,由于历史原因,1表示函数本身,和上面的0相同。所以在这里做-1操作
+func (f Frame) pc() uintptr {
+	return uintptr(f) - 1
+}
+
+// file 获取当前栈帧对应函数所在的文件名
+func (f Frame) file() string {
+	// runtime.FuncForPC 函数可以把程序计数器地址对应的函数信息获取出来
+	fn := runtime.FuncForPC(f.pc())
+	if fn == nil {
+		return "unknown"
+	}
+	file, _ := fn.FileLine(f.pc())
+	return file
+}
+
+// line 获取当前栈帧对应函数所在的行号
+func (f Frame) line() int {
+	fn := runtime.FuncForPC(f.pc())
+	if fn == nil {
+		return 0
+	}
+	_, line := fn.FileLine(f.pc())
+	return line
+}
+
+// name 获取当前栈帧对应函数的函数名称
+func (f Frame) name() string {
+	fn := runtime.FuncForPC(f.pc())
+	if fn == nil {
+		return "unknown"
+	}
+	return fn.Name()
+}
+
+// Format 栈帧对应的函数进行格式化输出。
+// 实现 fmt.Formatter 接口用来支持fmt包格式化输出
+// %s  只打印文件名
+// %+s 打印文件完整路径和名称
+// %d  只打印行号
+// %n  只打印函数名称
+// %v  %s:%d
+// %+v %+s:%d
+func (f Frame) Format(s fmt.State, verb rune) {
+	switch verb {
+	case 's':
+		switch {
+		case s.Flag('+'):
+			_, _ = io.WriteString(s, f.name())
+			_, _ = io.WriteString(s, "\n\t")
+			_, _ = io.WriteString(s, f.file())
+		default:
+			_, _ = io.WriteString(s, path.Base(f.file()))
+		}
+	case 'd':
+		_, _ = io.WriteString(s, strconv.Itoa(f.line()))
+	case 'n':
+		_, _ = io.WriteString(s, f.name())
+	case 'v':
+		f.Format(s, 's')
+		_, _ = io.WriteString(s, ":")
+		f.Format(s, 'd')
+	}
+}
+
+// MarshalText 序列化为字符串输出
+// 实现 encoding.TextMarshaler 接口
+func (f Frame) MarshalText() ([]byte, error) {
+	name := f.name()
+	if name == "unknown" {
+		return []byte(name), nil
+	}
+	return []byte(fmt.Sprintf("%s %s:%d", name, f.file(), f.line())), nil
+}
+
+// StackTrace 堆栈结构
+type StackTrace []Frame
+
+// Format 整个调用栈的格式化输出
+// 实现 fmt.Formatter 接口用来支持fmt包格式化输出
+// %s  堆栈输出格式为数组字符串
+// %v  堆栈输出格式为数组字符串
+// %+v 输出堆栈中每个栈帧的为文件名 函数名 行号,使用换行分隔。
+func (st StackTrace) Format(s fmt.State, verb rune) {
+	switch verb {
+	case 'v':
+		switch {
+		case s.Flag('+'):
+			for _, f := range st {
+				_, _ = io.WriteString(s, "\n")
+				f.Format(s, verb)
+			}
+		case s.Flag('#'):
+			fmt.Fprintf(s, "%#v", []Frame(st))
+		default:
+			st.formatSlice(s, verb)
+		}
+	case 's':
+		st.formatSlice(s, verb)
+	}
+}
+
+// formatSlice 将堆栈转换为数组字符串格式输出
+func (st StackTrace) formatSlice(s fmt.State, verb rune) {
+	_, _ = io.WriteString(s, "[")
+	for i, f := range st {
+		if i > 0 {
+			_, _ = io.WriteString(s, " ")
+		}
+		f.Format(s, verb)
+	}
+	_, _ = io.WriteString(s, "]")
+}
+
+// stack 程序计数器的堆栈
+type stack []uintptr
+
+// Format 直接格式化输出
+func (s *stack) Format(st fmt.State, verb rune) {
+	switch verb {
+	case 'v':
+		switch {
+		case st.Flag('+'):
+			for _, pc := range *s {
+				f := Frame(pc)
+				fmt.Fprintf(st, "\n%+v", f)
+			}
+		}
+	}
+}
+
+// StackTrace 转换为封装好的堆栈信息 StackTrace
+func (s *stack) StackTrace() StackTrace {
+	f := make([]Frame, len(*s))
+	for i := 0; i < len(f); i++ {
+		f[i] = Frame((*s)[i])
+	}
+	return f
+}
+
+// 生成堆栈的程序计算器
+func callers() *stack {
+	const depth = 32
+	var pcs [depth]uintptr
+	n := runtime.Callers(3, pcs[:])
+	var st stack = pcs[0:n]
+	return &st
+}
+
+// funcname 获取函数名。去除路径和包前缀
+func funcname(name string) string {
+	i := strings.LastIndex(name, "/")
+	name = name[i+1:]
+	i = strings.Index(name, ".")
+	return name[i+1:]
+}

+ 81 - 0
examples/errors/main.go

@@ -0,0 +1,81 @@
+package main
+
+import (
+	"fmt"
+	"net/http"
+
+	"days-gone/errors"
+)
+
+const (
+	ErrDatabase = iota + 100000
+	ErrEncodingFailed
+)
+
+func init() {
+	errors.RegisterCode(ErrDatabase, http.StatusInternalServerError, "数据错误", "")
+	errors.RegisterCode(ErrEncodingFailed, http.StatusInternalServerError, "绑定错误", "")
+}
+
+func main() {
+	if err := bindUser(); err != nil {
+		// %s: Returns the user-safe error string mapped to the error code or the error message if none is specified.
+		fmt.Println("====================> %s <====================")
+		fmt.Printf("%s\n\n", err)
+
+		// %v: Alias for %s.
+		fmt.Println("====================> %v <====================")
+		fmt.Printf("%v\n\n", err)
+		//
+		// %-v: Output caller details, useful for troubleshooting.
+		fmt.Println("====================> %-v <====================")
+		fmt.Printf("%-v\n\n", err)
+		//
+		// %+v: Output full error stack details, useful for debugging.
+		fmt.Println("====================> %+v <====================")
+		fmt.Printf("%+v\n\n", err)
+		//
+		// %#-v: Output caller details, useful for troubleshooting with JSON formatted output.
+		fmt.Println("====================> %#-v <====================")
+		fmt.Printf("%#-v\n\n", err)
+		//
+		// %#+v: Output full error stack details, useful for debugging with JSON formatted output.
+		fmt.Println("====================> %#+v <====================")
+		fmt.Printf("%#+v\n\n", err)
+		//
+		// do some business process based on the error type
+		if errors.IsCode(err, ErrEncodingFailed) {
+			fmt.Println("this is a ErrEncodingFailed error")
+		}
+		//
+		if errors.IsCode(err, ErrDatabase) {
+			fmt.Println("this is a ErrDatabase error")
+		}
+
+		// we can also find the cause error
+		fmt.Println(errors.Cause(err))
+	}
+}
+
+func bindUser() error {
+	if err := getUser(); err != nil {
+		// Step3: Wrap the error with a new error message and a new error code if needed.
+		return errors.WrapC(err, ErrEncodingFailed, "encoding user 'Lingfei Kong' failed.")
+	}
+
+	return nil
+}
+
+func getUser() error {
+	if err := queryDatabase(); err != nil {
+		// Step2: Wrap the error with a new error message.
+		return errors.Wrap(err, "get user failed.")
+	}
+
+	return nil
+}
+
+func queryDatabase() error {
+	// Step1. Create error with specified error code.
+	return errors.WithCode(ErrDatabase, "user 'Lingfei Kong' not found.")
+}

+ 5 - 0
go.mod

@@ -0,0 +1,5 @@
+module days-gone
+
+go 1.19
+
+require github.com/novalagung/gubrak v1.0.0

+ 10 - 0
go.sum

@@ -0,0 +1,10 @@
+github.com/DefinitelyMod/gocsv v0.0.0-20181205141819-acfa5f112b45 h1:+OD9vawobD89HK04zwMokunBCSEeAb08VWAHPUMg+UE=
+github.com/DefinitelyMod/gocsv v0.0.0-20181205141819-acfa5f112b45/go.mod h1:+nlrAh0au59iC1KN5RA1h1NdiOQYlNOBrbtE1Plqht4=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/novalagung/gubrak v1.0.0 h1:+iDvzUcSHUoa3bwP/ig40K2h9X+5cX2w5qcBb3izAwo=
+github.com/novalagung/gubrak v1.0.0/go.mod h1:lahTbjdK/OLI9Y4alRlf003XEwbiOj7ERkmDHFFbzLk=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=