学习Go踩过的坑

最近实习一直使用Go,总的来说Go是一个语法和C非常类似的语言,近年来在国内十分流行。Go的语法虽简单但死板,缺少一定灵活性,同python一样也有一些隐藏的坑,这里记录一下在日常使用中我亲自踩过的坑

Goroutine

线程之间变量共享

在Go中使用多线程十分简单,只要使用 go func() 即可
线程之间通信可以直接使用 channel, chan 类似于一个FIFO队列,使用 chan <- var 来发送变量, var := <- chan 来获取变量,如果chan中没有数据则会阻塞
Example:

ch := make(chan int)
go fun() {
    ch <- 1
}
a := <- ch
fmt.Println(a)
// output: "a"

但注意如果在循环中使用Goroutine,则会遇到局部变量的问题

nums := []int{1, 2, 3, 4, 5}
for _, x:= range nums{
    go func() {
        fmt.Println(x)
    }
}
// output:
// "6"
// "6"
// "6"
// "6"
// "6"

这里打印的变量和预期不符,是因为goroutine并不会立即启动,而x是循环变量,所以有可能读取的值并不是我们想要的
有如下解决方式

  1. 使用局部变量
nums := []int{1, 2, 3, 4, 5, 6}
for _, x:= range nums{
    i := x
    go func() {
        fmt.Println(i)
    }
}
// output:
// "1"
// "2"
// "3"
// "4"
// "5"
  1. 使用函数传值
nums := []int{1, 2, 3, 4, 5, 6}
for _, x:= range nums{
    go func(i int) {
        fmt.Println(i)
    } (x)
}
// output:
// "1"
// "2"
// "3"
// "4"
// "5"

XORM

xorm是go上比较常用的框架,但是坑很多,已经踩过很多

自定义SQL

编写自定义sql时,无法再使用自带的任何筛选条件

sql := "SELCET * FROM table"
obj, err := engine.SQL(sql)

这样写是没问题的
但是如果使用

sql := "SELCET * FROM table"
obj, err := engine.Desc("col").SQL(sql)

这种自带的查询条件则不会应用

批量更新

// 注意 xorm 插入多个记录时不支持同时更新自增主键!!!
// 详情 见 https://lunny.gitbooks.io/xorm-manual-zh-cn/content/chapter-04/index.html
func (t MyModelTable) InsertAll(models []*MyModel, sessions ...*xorm.Session) ([]*MyModel, error) {
	session, err := GetSession(sessions...)
	if err != nil {
		return nil, fmt.Errorf("get session error: %s", err)
	}
	_, err = session.Insert(&models)
	if err != nil {
		return nil, fmt.Errorf("db insert error: %s", err)
	}
	return models, nil
}

像这种在java里一般会吧id帮你更新成新插入tuple的主键,但是xorm在插入多条记录时不会更新主键, 如果要更新需要使用for 循环+ InsertOne的方式

gRPC

实习期间一直使用gRPC-go框架,国内这方面文档不多,有时候会遇到坑

grpc_auth

今天在用grpc middleware 中 grpc_auth的做验证的时候发现不能排除掉特定的endpoint的验证,想自己实现。后面发现grpc_auth包其实包括了这一功能
先实现自己的验证function

server := grpc.NewServer(
	grpc.MaxRecvMsgSize(MaxGRPCMessageSize),
	grpc.MaxSendMsgSize(MaxGRPCMessageSize),

	grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(
		grpc_recovery.StreamServerInterceptor(),
		// 注册服务的时候这里添加2个grpc_auth inteceptor			
		grpc_auth.StreamServerInterceptor(MyAuth),
	)),

	grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
		grpc_prometheus.UnaryServerInterceptor,
		grpc_recovery.UnaryServerInterceptor(),
		// 注册服务的时候这里添加2个grpc_auth inteceptor	
		grpc_auth.UnaryServerInterceptor(MyAuth),
		
	)))

这里MyAuth是自己实现的验证类,从验证header Authorization是否存在

var MyAuth = grpc_auth.AuthFunc(func(ctx context.Context) (context.Context, error) {
	headers, ok := metadata.FromIncomingContext(ctx)
	if !ok || len(headers.Get("Authorization")) == 0 {
		return ctx, status.Errorf(codes.Unauthenticated, "no x-aip-username header")
	}
	user := headers.Get("Authorization")[0]
	//user := "myUser"
	if user == "" {
		return ctx, status.Errorf(codes.Unauthenticated, "error")
	}
	newCtx := context.WithValue(ctx, "username", user)
	return newCtx, nil
})

//然后通过下面函数提取username

func GetUsername(ctx context.Context) string {
	if ctx == nil {
		return ""
	}
	if user, ok := ctx.Value("username").(string); ok {
		return user
	}
	return ""
}

这样就实现了全局拦截,如果不想拦截特定的endpoint,对应的server可以实现ServiceAuthFuncOverride接口

// AuthFuncOverride 覆盖grpc auth
// 实现ServiceAuthFuncOverride 接口
func (s myServer) AuthFuncOverride(ctx context.Context, fullMethodName string) (context.Context, error) {
	switch fullMethodName {
	// 不对/endpoint/path 做验证
	case "/endpoint/path":
		return ctx, nil
	default:
		return MyAuth(ctx)
	}
}

参考

grpc-gateway

如果需要在grpc中使用http接口,则需要使用grpc-gateway这个中间件。将http请求转为protobuf再传入grpc-server。protobuf文件中引用googple.api.http,可以自动生成grpc-gateway的代码
这中间有很多无形的坑

参考

gogo-proto

protobuf3中如果一个变量不赋值,那么会自动赋该变量的默认值,例如int64的默认值是0,bool的默认值是false等。
开发时应特别注意,默认情况下,包括enum值默认也会是设置的第一个值,使用proto3是无法区别0值和空值的,go中也是,尽量避免给0值添加特殊意义。

启用0值

protobuf 输出0值

如果想输出0值,gRPC自带的proto和json marshal工具是无法做到的。
需要引用

	github.com/gogo/gateway 
	github.com/gogo/protobuf 

这两个依赖,如过proto
要输出0值,需要在使用protoc生成代码时引用github.com/gogo/protobuf,并且在要使用该功能的protobuf文件中添加

// Enable custom Marshal method.
option (gogoproto.marshaler_all) = true;
// Enable custom Unmarshal method.
option (gogoproto.unmarshaler_all) = true;
// Enable registration with golang/protobuf for the grpc-gateway.
option (gogoproto.goproto_registration) = true;
// Enable generation of XXX_MessageName methods for grpc-go/status.
option (gogoproto.messagename_all) = true;
json输出0值

NewServeMux(http-server)时,手动修改JSON marshaler的参数,如下

gwMux := runtime.NewServeMux(
		runtime.WithIncomingHeaderMatcher(func(s string) (string, bool) {
			return s, true
		}),
		runtime.WithOutgoingHeaderMatcher(func(s string) (string, bool) {
			return s, true
		}),
		// 添加下面的字段
		runtime.WithMarshalerOption(runtime.MIMEWildcard, &gateway.JSONPb{OrigName: true, EmitDefaults: true}),
	)

手动替换json marshaler为gogo/gateway包里的JSONPb, 并添加参数 EmitDefaults = true 即可

gRPC header问题

在grpc升级到1.42.0后,server会把connection header认为是 malformed header 而拒绝(详见 grpc-go 1.42.0 release note)
而grpc-gateway会把合法的http header加上自己的prefix后透传给grpc server变成grpc header。
而http connection header 就不会添加prefix,所以 如果通过grpc-gateway接收的http请求包含 Connect: keep-alive 这个header,服务器会直接拒绝该请求

解决办法

替换grpc-gateway默认的headerMatcher, 将connect header加上prefix即可

// new http mux server时,加入headerMatcher中间件
gwMux := runtime.NewServeMux(
                 // 加入自定义的header matcher
		runtime.WithIncomingHeaderMatcher(func(s string) (string, bool) {
			return myDefaultHeaderMatcher(s)
		}),
		runtime.WithOutgoingHeaderMatcher(func(s string) (string, bool) {
			return s, true
		}),
		runtime.WithForwardResponseOption(httpResponseModifier),
		runtime.WithMarshalerOption(runtime.MIMEWildcard, &gateway.JSONPb{
			OrigName:     true,
			EmitDefaults: true,
		}),
	)
// myDefaultHeaderMatcher,修改自github.com/grpc-ecosystem/grpc-gateway/runtime.WithHeaderMatcher中的函数
// replacement for default HeaderMatcher
// same as runtime.WithHeaderMatcher
// did some modification for header matcher
// see https://github.com/grpc-ecosystem/grpc-gateway/issues/2447
myDefaultHeaderMatcher := func(key string) (string, bool) {
	key = textproto.CanonicalMIMEHeaderKey(key)
	// use modifier http header function
	if isPermanentHTTPHeader(key) {
		return runtime.MetadataPrefix + key, true
	} else if strings.HasPrefix(key, runtime.MetadataHeaderPrefix) {
		return key[len(runtime.MetadataPrefix):], true
	}
	// return header no matter what
	return key, true
}

// 自定义的http header判断函数, 加入connection header
func isPermanentHTTPHeader(hdr string) bool {
	// after google.golang.org/grpc v1.42.0, grpc will reject connection header
	// we add it to header modifier manually
	// see https://github.com/grpc/grpc-go/issues/5175
	switch hdr {
	case
		"Accept",
		"Accept-Charset",
		"Accept-Language",
		"Accept-Ranges",
		"Authorization",
		"Cache-Control",
		"Content-Type",
		"Cookie",
		"Date",
		"Expect",
		"From",
		"Host",
		"If-Match",
		"If-Modified-Since",
		"If-None-Match",
		"If-Schedule-Tag-Match",
		"If-Unmodified-Since",
		"Max-Forwards",
		"Origin",
		"Pragma",
		"Referer",
		"User-Agent",
		"Via",
		"Warning",
		// add connection header

		"Connection":
		return true
	}
	return false
}

参考