Golang Code Review

输入校验

\

validator

GitHub - go-playground/validator: :100:Go Struct and Field validation, including Cross Field, Cross Struct, Map, Slice and Array diving

常见使用

// bad code

if len(ids) == 0 {

    return nil, errors.New("ids is empty")

}

if objectType != "question" && objectType != "answer" && objectType != "article" && objectType != "zvideo" && objectType != "pin" {

    return nil,  errors.New("content objectType is invalid")

}

// except

type ContentMaaSSchema struct {

    Ids        []int64 `json:"ids" bind:"required"`

    ObjectType string  `json:"object_type" bind:"required,ascii,one-of=question answer article zvideo pin"`

}

var input ContentMaaSSchema

if err := defaultValidator.ValidateStruct(input); err != nil {

    HandleError(w, err)

    return

}

操作符说明

标记

标记说明

, 多操作符分割
| 或操作
- 跳过字段验证

常用标记

 点击此处展开...

标记

标记说明

required 必填 Field 或 Struct validate:"required"
omitempty 空时忽略 Field 或 Struct validate:"omitempty"
len 长度 Field validate:"len=0"
eq 等于 Field validate:"eq=0"
gt 大于 Field validate:"gt=0"
gte 大于等于 Field validate:"gte=0"
lt 小于 Field validate:"lt=0"
lte 小于等于 Field validate:"lte=0"
eqfield 同一结构体字段相等 Field validate:"eqfield=Field2"
nefield 同一结构体字段不相等 Field validate:"nefield=Field2"
gtfield 大于同一结构体字段 Field validate:"gtfield=Field2"
gtefield 大于等于同一结构体字段 Field validate:"gtefield=Field2"
ltfield 小于同一结构体字段 Field validate:"ltfield=Field2"
ltefield 小于等于同一结构体字段 Field validate:"ltefield=Field2"
eqcsfield 跨不同结构体字段相等 Struct1.Field validate:"eqcsfield=Struct2.Field2"
necsfield 跨不同结构体字段不相等 Struct1.Field validate:"necsfield=Struct2.Field2"
gtcsfield 大于跨不同结构体字段 Struct1.Field validate:"gtcsfield=Struct2.Field2"
gtecsfield 大于等于跨不同结构体字段 Struct1.Field validate:"gtecsfield=Struct2.Field2"
ltcsfield 小于跨不同结构体字段 Struct1.Field validate:"ltcsfield=Struct2.Field2"
ltecsfield 小于等于跨不同结构体字段 Struct1.Field validate:"ltecsfield=Struct2.Field2"
min 最大值 Field validate:"min=1"
max 最小值 Field validate:"max=2"
structonly 仅验证结构体,不验证任何结构体字段 Struct validate:"structonly"
nostructlevel 不运行任何结构级别的验证 Struct validate:"nostructlevel"
dive 向下延伸验证,多层向下需要多个dive标记 [][]string validate:"gt=0,dive,len=1,dive,required"
dive Keys & EndKeys 与dive同时使用,用于对map对象的键的和值的验证,keys为键,endkeys为值 map[string]string validate:"gt=0,dive,keys,eq=1\|eq=2,endkeys,required"
required_with 其他字段其中一个不为空且当前字段不为空 Field validate:"required_with=Field1 Field2"
required_with_all 其他所有字段不为空且当前字段不为空 Field validate:"required_with_all=Field1 Field2"
required_without 其他字段其中一个为空且当前字段不为空 Field `validate:“required_without=Field1 Field2”
required_without_all 其他所有字段为空且当前字段不为空 Field validate:"required_without_all=Field1 Field2"
isdefault 是默认值 Field validate:"isdefault=0"
oneof 其中之一 Field validate:"oneof=5 7 9"
containsfield 字段包含另一个字段 Field validate:"containsfield=Field2"
excludesfield 字段不包含另一个字段 Field validate:"excludesfield=Field2"
unique 是否唯一,通常用于切片或结构体 Field validate:"unique"
alphanum 字符串值是否只包含 ASCII 字母数字字符 Field validate:"alphanum"
alphaunicode 字符串值是否只包含 unicode 字符 Field validate:"alphaunicode"
alphanumunicode 字符串值是否只包含 unicode 字母数字字符 Field validate:"alphanumunicode"
numeric 字符串值是否包含基本的数值 Field validate:"numeric"
hexadecimal 字符串值是否包含有效的十六进制 Field validate:"hexadecimal"
hexcolor 字符串值是否包含有效的十六进制颜色 Field validate:"hexcolor"
lowercase 符串值是否只包含小写字符 Field validate:"lowercase"
uppercase 符串值是否只包含大写字符 Field validate:"uppercase"
email 字符串值包含一个有效的电子邮件 Field validate:"email"
json 字符串值是否为有效的 JSON Field validate:"json"
file 符串值是否包含有效的文件路径,以及该文件是否存在于计算机上 Field validate:"file"
url 符串值是否包含有效的 url Field validate:"url"
uri 符串值是否包含有效的 uri Field validate:"uri"
base64 字符串值是否包含有效的 base64值 Field validate:"base64"
contains 字符串值包含子字符串值 Field validate:"contains=@"
containsany 字符串值包含子字符串值中的任何字符 Field validate:"containsany=abc"
containsrune 字符串值包含提供的特殊符号值 Field validate:"containsrune=☢"
excludes 字符串值不包含子字符串值 Field validate:"excludes=@"
excludesall 字符串值不包含任何子字符串值 Field validate:"excludesall=abc"
excludesrune 字符串值不包含提供的特殊符号值 Field validate:"containsrune=☢"
startswith 字符串以提供的字符串值开始 Field validate:"startswith=abc"
endswith 字符串以提供的字符串值结束 Field validate:"endswith=abc"
ip 字符串值是否包含有效的 IP 地址 Field validate:"ip"
ipv4 字符串值是否包含有效的 ipv4地址 Field validate:"ipv4"
datetime 字符串值是否包含有效的 日期 Field validate:"datetime"

并发场景

\

Goroutine

1、sync.WaitGroup

defer func() {

    if re := recover(); re != nil {

        log.Errorf(s.ctx, "panic panic, err: %+v", re)

    }

    wg.Done() // ***

}()

resChan := make(chan interface{}, 3)

wg.Add(3)

go queryZRender(&resChan, objectIDs)

go queryTagCore(&resChan, objectIDs)

go queryZIndex(&resChan, objectIDs, indexesTypeContent, timelinessPT)

wg.Wait()

close(resChan)

2、future

// 设计

package future

import (
	"context"
	"errors"
	"fmt"

	pkgErrors "github.com/pkg/errors"
)

type Future struct {
	ch  chan struct{}
	fn  Func
	err error
}

type Func func() error

func New(fn Func) *Future {
	f := &Future{
		ch: make(chan struct{}),
		fn: fn,
	}
	f.start()
	return f
}

func (f *Future) start() {
	go func() {
		defer func() {
			if rval := recover(); rval != nil {
				if err, ok := rval.(error); ok {
					f.err = pkgErrors.WithStack(err)
				} else {
					rvalStr := fmt.Sprint(rval)
					f.err = pkgErrors.WithStack(errors.New(rvalStr))
				}
			}

			close(f.ch)
		}()

		// 执行结果
		f.err = f.fn()
		return
	}()
}

func (f *Future) Get(ctx context.Context) error {
	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-f.ch:
		return f.err
	}
}

func (f *Future) Done() bool {
	select {
	case <-f.ch:
		return true
	default:
		return false
	}
}

// 使用

futures := make([]*future.Future, 0)

futures = append(futures, future.New(func() error {

    claims, e := c.creatorClaimDao.BatchGet(ctx, memberIDs)

    for _, claim := range claims {

        claimMap[claim.MemberID] = claim

    }

    return e

}))

futures = append(futures, future.New(func() error {

    users, _ := c.userDao.BatchGet(ctx, userIDs)

    for _, user := range users {

        userMap[user.UserID] = user

    }

    return err

}))

futures = append(futures, future.New(func() error {

    memberMap = c.memberRpc.MBatchGetMemberByID(ctx, memberIDs)

    return nil

}))

for _, fu := range futures {

    e := fu.Get(ctx)

    if e != nil {

        log.Error(ctx, e)

    }

}

一些常见并发编程错误

golang中常见的并发bug

闭包使用

for 循环里面使用 Goroutine

for obj := range objs {

    go func() {

        do(obj)

    }()

}

for obj := range objs {

    go func(obj ObjType) {

        do(obj)

    }(obj)

}

第一种是直接在闭包里面使用 obj,这个时候其实 obj 是一个引用,goroutine 的执行时间是不确定的,可能等你进行第 n 遍循环的时候,n - x 轮的 go 才开始执行,这个时候获取到的 obj 的值,已经不再是你希望的那个 obj 了

Gorm

  • gorm.io/gorm
    • GORM 中文文档
    • 代码模块化
    • Context,批量插入,预编译模式,DryRun 模式,Join 预加载,Find To Map,Create From Map,FindInBatches 支持
    • 支持嵌套事务,SavePoint,Rollback To SavePoint
    • 全新的 Hook API:带插件的统一接口
    • 统一命名策略:表名、字段名、连接表名、外键、检查器、索引名称规则
    • 更好的自定义类型支持(例如: JSON)
  • database/sql 在处理字段类型转换的时候抛了异常,解决办法:
    • 基于SQL库的查询需要注意结果字段为NULL的情况,对应结构体字段需要声明为指针类型
  • gorm.io/gorm de 处理办法:
  •  点击此处展开...

    // assign stmt.ReflectValue

    if stmt.Dest != nil {

        stmt.ReflectValue = reflect.ValueOf(stmt.Dest)

        for stmt.ReflectValue.Kind() == reflect.Ptr {

            if stmt.ReflectValue.IsNil() && stmt.ReflectValue.CanAddr() {

                stmt.ReflectValue.Set(reflect.New(stmt.ReflectValue.Type().Elem()))

            }

            stmt.ReflectValue = stmt.ReflectValue.Elem()

        }

        if !stmt.ReflectValue.IsValid() {

            db.AddError(ErrInvalidValue)

        }

    }

格式转换

  • github.com/spf13/cast
  • strconv
    • strconv.ParseInt()
    • strconv.ParseFloat()
    • strconv.ParseBool()

格式化规范

  • gofmt
    • 代码风格检查:保存时自动格式化代码
  • goimports
    • 保存时自动导入处理包
    • 导入多种类型的包,建议以「标准库」>「依赖库」>「本地包」的顺序排序,使用 goimports -w -local git.in.xxx.com/user/project . 会将 import 自动排版

       点击此处展开...

      import (

         // builtin packages

         "context"

        

         // other packages

         "git.in.zhihu.com/go/box/mysql"

         "git.in.zhihu.com/zhihu/demo/pkg/dao"

      )

  • GolangCI-Lint

    • 静态代码检查:集成常用的代码检查工具,并提供统一的配置入口和输出格式

       点击此处展开...

      Makefile

      lint:

         $(ENV) $(GOPATH)/bin/golangci-lint run --deadline=5m ./pkg/...

         $(ENV) $(GOPATH)/bin/golangci-lint run --deadline=5m ./cmd/...

        

      test: lint

         go test -coverprofile=coverage.out ./pkg/...

      joker.yml

      test:

        unittest:

          - make test

    • godoc:文档注释

其他

  • 错误包装:和第三方(RPC、MySQL...)交互返回错误时建议通过 "pkg/errors".Wrap 或者 "pkg/errors".Wrapf 来添加上下文信息,而不是将错误直接透传至上层

  • 禁用 panic:在生产环境中运行的代码必须避免出现 panic,panic/recover 不是错误处理策略
  • 指定容器容量:向make()提供容量提示会在初始化时尝试调整map的大小,这将减少在将元素添加到map时为map重新分配内存。
    • 与maps不同,slice capacity不是一个提示:编译器将为提供给make()的slice的容量分配足够的内存
  • 建议增加在函数声明中如果需要传入 context,context 需要作为第一个参数传入
  • nil 是一个有效的 slice

你可能感兴趣的:(golang,代码复审)