学习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是循环变量,所以有可能读取的值并不是我们想要的
有如下解决方式
- 使用局部变量
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"
- 使用函数传值
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)
}
}
参考
- https://github.com/grpc-ecosystem/go-grpc-middleware/issues/176
- https://madmalls.com/blog/post/grpc-authentication/
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
}
参考