推荐使用Golang进行开发

首先

加入了PJ项目,在其中构建一个基于golang和gPRC的API,我花了大约3个月的时间学习了Go语言,所以我会总结出开发Go时需要注意的要点。这篇文章的目标读者是刚开始使用Go进行开发的人,或者已经开始使用Go开发一段时间的人,就像我一样。

我试用了一段时间Go语言后的感受是,Go语言在自由度方面无论是好也好坏也好,都给人一种主张强烈的语言的印象。由于没有泛型,有时需要写多个只是类型不同的类似复制粘贴的代码,这可能会让人感到烦恼;还有在一些情况下,强制使用制表符代替空格也可能会让人感到不悦。虽然有人说“入郷而从郷”(鵜飼文敏先生),认为这就是Go的特色,我也认为或许确实如此。

关于开发环境

主要关注开发环境和工具方面的要点

使用 Go Modules

在 Go 语言中,有一个特殊的概念叫做 GOPATH。这意味着所有的 Go 代码,包括第三方库,都必须放在 $GOPATH/src 目录下。

我认为,作为职业工程师,我们希望将代码根据项目或客户进行分类而不是根据模块的名称进行分类。要实现这一点,Go 1.11引入了Go Modules模式。通过启用Go Modules模式,您将不再受到代码存放位置的限制。要启用此模式,请将环境变量中的GO111MODULE=on设置添加进去。

$ export GO111MODULE=on

顺便提一下,据说在Go 1.13中,Go模块模式默认是启用的1。
上述的环境变量可能是不必要的。

为了使用Go Modules,需要使用init命令来生成go.mod文件。

$ go mod init <module名>

请输入实际部署该模块的域名作为模块名称。
例如,在GitHub上进行开发,模块名称可能是类似于github.com/tomtwinkle/go-sample的路径。

使用 Go Modules 可以参考本地仓库。

当在使用Go Modules模式进行开发,像Goland等IDE时,会查看go.mod文件的内容,然后自动获取最新的模块到master分支。然而,如果还在开发依赖的库,这样做可能会导致困扰。因此,有时候希望能够引用本地正在开发的目录。

可以通过在 go.mod 文件中添加 replace => 的配置来引用本地目录,达到该目的。

module github.com/tomtwinkle/go-sample

go 1.13

replace github.com/tomtwinkle/go-lib => ../go-lib

require (
    github.com/tomtwinkle/go-lib v1.0.0
)

在进行推送之前进行 go fmt 和 go vet 的操作。

这是关于开发环境的规则,更像是遵守它将使自己和其他成员变得更加幸福的规则。
在Go的标准工具中有一个叫做”go fmt”和”go vet”的代码格式化和代码检查工具。
在将代码推送到仓库之前,请务必使用上述两个命令对代码进行格式化和修复语法错误。

在本地环境下执行go fmt是个好选择。
或许可以将go vet整合到CI中。

每次都会忘记敲门的人,像下面的文章所述,似乎最好是在git的pre-commit上创建一个hook脚本。
在使用git管理Go语言程序时非常方便。

将仅在内部使用的软件包放置在内部的”internal”中。

以下的网站可以参考了解内部 package 是什么:
https://mattn.kaoriya.net/software/lang/go/20150820102400.htm

    • internal/hoge

 

    internal/fuga

将hoge fuga包在internal中设置为私有,如果从外部引用将会导致错误。

使用 GoLand

这是一句直截了当的话,如果要使用Go进行开发,就使用JetBrains的GoLand吧。
GoLand本身也很容易使用,但它之所以吸引人是因为它有完整的为Go开发准备的插件。
使用File Watchers插件,在文件保存时自动执行go fmt和go vet,仅仅这样就可以提高生产效率约1.2倍(个人经验)。
※本文纯属个人观点,与我的雇主毫无关系。

有关开发时的代码和审查要点

包裝結構

现在的项目在参考以下内容的基础上,根据清晰架构的概念构建软件包结构。
https://github.com/golang-standards/project-layout

关于 Clean Architecture,在这里写起来太长,所以以后再说。
如果使用 Go Modules,上述的 vendor 文件夹是不必要的。

错误处理

由于Go语言中没有异常处理机制,像其他语言中的try/catch那样捕获函数内发生的所有错误是不太可能的(尽管通过panic/recover机制可以实现)。我认为panic应该只在真正严重到必须停止系统的情况下使用,而不是用于其他非常严重的错误。至少目前为止,我还没有遇到过必须编写recover的代码机会。

虽然关于Go语言的错误处理已经在其他网站上被讨论得很多了,但是如果掌握以下要点,会很有帮助。

    1. 在错误发生后,立即返回并不给予处理的余地。

 

    1. 在产生错误时,尽量将函数内的信息进行包装。

 

    1. 在返回错误时,务必将该错误记录到堆栈中。

 

    1. 使用pkg/errors可以更轻松地处理上述情况。

 

    错误日志应尽量在靠近主要代码的位置进行集中处理。

NG模式

func main() {
    ctx := context.Background()
    sample(ctx)
}

func sample(ctx context.Context) {
    logger := ctxzap.Extract(ctx)

    user, err := getUser()
    if err == nil && user != nil {
        logger.Info(fmt.Sprintf("I'm %s !", user.firstName))
    }
    if err != nil {
        logger.Error(err.Error())
    }
}

在调用 getUser() 函数后捕获了错误之后,立即放入某种处理,实际上可能不会进入该处理流程,但通过功能增加等可能不再保证。

好的模式

func main() {
    ctx := context.Background()
    logger := ctxzap.Extract(ctx)
    err := sample(ctx)
    if err != nil {
        logger.Error(fmt.Sprintf("%+v", err))
    }
}

func sample(ctx context.Context) error {
    hoge := "sample"
    err := sampleChild(ctx, hoge)
    if err != nil {
        return errors.WithStack(err)
    }
    return nil
}

func sampleChild(ctx context.Context, hoge string) error {
    logger := ctxzap.Extract(ctx)

    user, err := getUser(hoge)
    if err != nil {
        return errors.Wrapf(err, "user not found. arg=%s", hoge)
    }
    logger.Info(fmt.Sprintf("I'm %s !", user.firstName))
    return nil
}

在调用返回错误的函数后,立即使用`if err != nil {}`来检查错误的存在与否,并且如果是错误的情况下,立即返回。
一旦开始编写Go代码,你会经常看到这种`if err != nil {}`的语法。

输出错误日志

关于错误日志的输出,我们正在积极探索,因此正在征求这样的建议。
顺便提一句,在上述情况中,我们会在主函数内捕获错误日志,但对于 gRPC 服务器,我们可以使用 go-grpc-middleware,在 WithUnaryServerChain 中(或者在使用服务器端流时,在 WithStreamServerChain 中),通过自定义拦截器链来捕获错误日志似乎是更好的选择。

gRPC Server的错误日志输出package main

import (
“context”
“fmt”
grpcMiddleware “github.com/grpc-ecosystem/go-grpc-middleware”
grpcZap “github.com/grpc-ecosystem/go-grpc-middleware/logging/zap”
“github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap”
grpcCtxtags “github.com/grpc-ecosystem/go-grpc-middleware/tags”
“github.com/pkg/errors”
“go.uber.org/zap”
“google.golang.org/grpc”
“log”
“net”
customInterceptor “sample”
)

func main() {
c := zap.NewProductionConfig()
c.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
logger, err := c.Build()
if err != nil {
log.Fatal(err)
}

lis, err := net.Listen(“tcp”, fmt.Sprintf(“:%s”, “50051”))

s := grpc.NewServer(
grpcMiddleware.WithUnaryServerChain(
customInterceptor.UnaryServerInterceptor(),
grpcCtxtags.UnaryServerInterceptor(grpcCtxtags.WithFieldExtractor(grpcCtxtags.CodeGenRequestFieldExtractor)),
grpcZap.UnaryServerInterceptor(logger),
),
grpcMiddleware.WithStreamServerChain(
customInterceptor.StreamServerInterceptor(),
grpcCtxtags.StreamServerInterceptor(grpcCtxtags.WithFieldExtractor(grpcCtxtags.CodeGenRequestFieldExtractor)),
grpcZap.StreamServerInterceptor(logger),
),
)
{ // 用户处理程序
// gRPC API处理程序
server := handler.NewUser()
// 生成gRPC服务器的protobuf接口
protobufInterface.RegisterUserServer(s, server)
}
if err := s.Serve(lis); err != nil {
log.Fatalf(“无法提供服务: %v”, err)
}
}

package custom_interceptor

import (
“context”
“fmt”
“github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap”
“github.com/pkg/errors”
“go.uber.org/zap”
“go.uber.org/zap/zapcore”
“google.golang.org/grpc”
)

type wrapServerStream struct {
grpc.ServerStream
c context.Context
}

type wrapCore struct {
zapcore.Core
}

func UnaryServerInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (_ interface{}, err error) {
logger := ctxzap.Extract(ctx)
logger = logger.WithOptions(zap.WrapCore(func(c zapcore.Core) zapcore.Core {
return &wrapCore{Core: c}
}))
ctx = ctxzap.ToContext(ctx, logger)

resp, err := handler(ctx, req)
if err != nil {
logger.Error(fmt.Sprintf(“%+v\n”, err))
return resp, errors.WithStack(err)
}
return resp, nil
}
}

func StreamServerInterceptor() grpc.StreamServerInterceptor {

return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
ctx := stream.Context()
logger := ctxzap.Extract(stream.Context())
logger = logger.WithOptions(zap.WrapCore(func(c zapcore.Core) zapcore.Core {
return &wrapCore{Core: c}
}))
ctx = ctxzap.ToContext(ctx, logger)

err := handler(srv, &wrapServerStream{ServerStream: stream, c: ctx})
if err != nil {
logger.Error(fmt.Sprintf(“%+v\n”, err))
return errors.WithStack(err)
}
return nil
}
}

如果在函数的参数或返回值中使用较大的结构体时,请使用指针。

这就是这样的吗?

// 引数:ポインター、戻り値:ポインターの場合
func GetSample(req *RequestSample) (*Sample, error) {}

// 引数:実体、戻り値:実体の場合
func GetSample(req RequestSample) (Sample, error) {}

尽管结构体大小也会影响,但每次复制结构体的成本很高。
如果要传递或返回大型结构体,最好使用指针。
特别是返回值为结构体的情况,经常会成为模型,几乎都可以用指针来处理。

由于将一切都转换为指针会使代码变得不够简洁,因此对于诸如int、string等基本类型的值,最好不要进行转换;对于像time.Time这样的小型结构体,直接传递实际值可能更加方便使用。

使用的是*T而不是T。

我不确定哪个更好,所以在纠结中找到了以下的文章。

returning []*T vs []T
byu/nesigma ingolang

在内存中与 T 直接相邻(运行时将分配 sizeof(T) * cap 的空间),这增加了缓存的一致性。例如,对于 []int 的循环可以非常高效甚至有可能进行矢量化处理。

访问切片元素需要进行拷贝,这比传递指针更加昂贵。同样,修改元素需要先拷贝元素,修改后再拷贝回去。

不需要拷贝就可以读写元素。

需要对存储的指针进行间接引用,指针可以指向 RAM 中的任意位置,而且可能无法充分利用缓存的一致性。

缓存的一致性还包括 RAM 预取;现代 CPU 架构十分复杂,但是顺序访问通常比随机访问更快。

如果T的结构体越大,那么在循环中处理它时的成本可能会更高,因为需要复制slice的元素。
在int和[]*int之间可能前者更好。

将 Context.Value 中放入仅在请求范围内完成的项目。

大家都使用Context.Value吗?
我还在纠结应该在什么范围内使用,
从评论的角度来看,
暂且定义为在请求范围内创建并在请求范围结束时消失的对象,可以放进去。

可以放进去的东西

    • request の metadata (ユーザID や ユーザ名 や browser情報など)

 

    request 時刻

微妙但仍然包含着一些东西

    データベースのtransactionオブジェクト

可能是不允许放进去的物品

    • アプリのバージョン情報

 

    • データベースのconnectionオブジェクト

 

    ステートフルなユーザのsession情報

不使用 testify/assert.Equal(),而是使用 cmd.Diff() 来进行断言。

在测试/断言时,assert.Equal()使用了reflect.DeepEqual函数。reflect.DeepEqual函数可以比较包含time.Time类型的结构体,因为它可以访问到结构体的非公开值(私有)。因此,在编写测试时需要考虑更多的事项。

    • 取得した構造体の unexport value を参照するようなケースはない

 

    • Getter で取得するならその構造体の Getter のテストを書けばよい

 

    • ドメイン駆動なモデルで unexport value により export value が変化するなら

 

    そのモデルのテストを書けばよい

从上述角度来看,在测试的assert方面,似乎使用cmd.Diff()会更好。

断言.

import (
    "github.com/stretchr/testify/assert"
    "testing"
)

type Sample struct {
    Public  string
    private int64
}

func GetSample() (*Sample, error) {
    return &Sample{
        Public:  "a",
        private: int64(1),
    }, nil
}

func TestSample_GetSample(t *testing.T) {
    t.Run("assert.Equal", func(t *testing.T) {
        expected := &Sample{
            Public: "a",
        }
        actual, err := GetSample()
        if err != nil {
            t.Error(err.Error())
        }
        assert.Equal(t, expected, actual)
    })
}
--- FAIL: TestSample_GetSample (0.00s)
    --- FAIL: TestSample_GetSample/assert.Equal (0.00s)
        main_test.go:25: 
                Error Trace:    main_test.go:25
                Error:          Not equal: 
                                expected: &main.Sample{Public:"a", private:0}
                                actual  : &main.Sample{Public:"a", private:1}

                                Diff:
                                --- Expected
                                +++ Actual
                                @@ -2,3 +2,3 @@
                                  Public: (string) (len=1) "a",
                                - private: (int64) 0
                                + private: (int64) 1
                                 })
                Test:           TestSample_GetSample/AssertEqual


Expected :&main.Sample{Public:"a", private:0}
Actual   :&main.Sample{Public:"a", private:1}

cmp.Diff()的含义是什么?

根据选项的选择来确定当存在 unexport value 时的操作方式。由于默认情况下存在 unexport value 会导致恐慌,不便于使用,因此我们创建了 DeepEqual 工具以使其更易于使用。

import (
    "github.com/google/go-cmp/cmp"
    "github.com/google/go-cmp/cmp/cmpopts"
    "reflect"
    "testing"
)

type Sample struct {
    Public  string
    private int64
}

func GetSample() (*Sample, error) {
    return &Sample{
        Public:  "a",
        private: int64(1),
    }, nil
}

func TestSample_GetSample(t *testing.T) {
    t.Run("cmp.Diff", func(t *testing.T) {
        expected := &Sample{
            Public: "a",
        }
        actual, err := GetSample()
        if err != nil {
            t.Error(err.Error())
        }
        DeepEqual(t, expected, actual)
    })
}

func DeepEqual(t *testing.T, expected, actual interface{}, opts ...cmp.Option) bool {
    t.Helper()

    var opt cmp.Option
    e := reflect.ValueOf(expected)
    // cmpopts.IgnoreUnexported() オプション で Unexported な attribute を無視する 
    if e.Kind() == reflect.Ptr {
        // cmpopts.IgnoreUnexported() の引数には interface の実体を指定する必要がある
        opt = cmpopts.IgnoreUnexported(e.Elem().Interface())
    } else {
        opt = cmpopts.IgnoreUnexported(expected)
    }

    opts = append(opts, opt)
    if diff := cmp.Diff(expected, actual, opts...); diff != "" {
        t.Errorf("Diff: (-expected +actual)\n%s", diff)
        return false
    }
    return true
}
=== RUN   TestSample_GetSample/cmp.Diff
--- PASS: TestSample_GetSample (0.00s)
    --- PASS: TestSample_GetSample/cmp.Diff (0.00s)
PASS

参考网站

https://budougumi0617.github.io/2019/02/15/go-modules-on-go112/ 的内容可以用中国翻译为:
bannerAds