goimports就是等于
gofmt加上依赖包管理。
goimports,虽然
goimports有时会引入错误的包,但是与带来的好处相比,这些偶尔出现的错误在作者看来也是可以接受的;当然,不想使用
goimports的开发者也一定要在 IDE 或者编辑器中开启自动地
gofmt(保存时自动格式化)。
在 IDE 和 CI 检查中开启自动地
gofmt或者goimports检查是没有、也不应该有讨论的必要的,这就是一件使用和开发 Go 语言必须要做的事情。
golint了,作为官方提供的工具,它在可定制化上有着非常差的支持,我们只能通过如下所示的方式运行
golint对我们的项目进行检查:
[code]$ golint ./pkg/...[/code]
pkg/liquidity/liquidity_pool.go:18:2: exported var ErrOrderBookNotFound should have comment or be unexported
pkg/liquidity/liquidity_pool.go:23:6: exported type LiquidityPool should have comment or be unexported
pkg/liquidity/liquidity_pool.go:23:6: type name will be used as liquidity.LiquidityPool by other packages, and that stutters; consider calling this Pool
pkg/liquidity/liquidity_pool.go:31:1: exported function NewLiquidityPool should have comment or be unexported
...
golint定制化的 讨论,
golint的开发者给出了以下的几个观点解释为什么
golint不支持定制化的功能:
lint的目的就是在 Go 语言社区中鼓励统一、一致的编程风格,某些开发者也许不会同意其中的某些规范,但是使用统一的风格对于 Go 语言社区有比较强的好处,而能够开关指定规则的功能会导致golint不能够有效地完成这个工作;- 有一些静态检查的规则会导致一些错误的警告,这些情况确实非常让人头疼,但是我会选择支持在 golint 中直接保留或者删除这些规则,而不是随提供意增删规则的能力;
- 能够通过
min_confidence过滤一些静态检查规则,但是需要我们选择合适的值;
golint作者的观点在 issue 中得到了非常多的 ?,但是这件事情很难说对错;在社区中保证一致的编程规范是一件非常有益的事情,不过对于很多公司内部的服务或者项目,可能在业务服务上就会发生一些比较棘手的情况,使用这种过强的约束没有太多明显地收益。
golint进行静态检查(或者同时使用
golint和 golangci-lint),在其他的项目中使用可定制化的
golangci-lint来进行静态检查,因为在基础库和框架中施加强限制对于整体的代码质量有着更大的收益。
作者会在自己的 Go 项目中使用
golint+golangci-lint并开启全部的检查尽量尽早发现代码中包含文档在内的全部缺陷。
goimports还是静态检查工具
glint或者
golangci-lint,只要我们在项目中引入这些工具就一定要在代码的 CI 流程中加入对应的自动化检查:
[code]├── LICENSE.md[/code]
├── Makefile
├── README.md
├── api
├── assets
├── build
├── cmd
├── configs
├── deployments
├── docs
├── examples
├── githooks
├── init
├── internal
├── pkg
├── scripts
├── test
├── third_party
├── tools
├── vendor
├── web
└── website
/pkg目录是 Go 语言项目中非常常见的目录,我们几乎能够在所有知名的开源项目(非框架)中找到它的身影,例如:
import引入这里的代码,所以当我们将代码放入
pkg时一定要慎重,不过如果我们开发的是 HTTP 或者 RPC 的接口服务或者公司的内部服务,将私有和公有的代码都放到
/pkg中也没有太多的不妥,因为作为最顶层的项目来说很少会被其他应用直接依赖,当然严格遵循公有和私有代码划分是非常好的做法,作者也建议各位开发者对项目中公有和私有的代码进行妥善的划分。私有代码
/internal目录中,真正的项目代码应该写在
/internal/app里,同时这些内部应用依赖的代码库应该在
/internal/pkg子目录和
/pkg中,下图展示了一个使用
/internal目录的项目结构:
internal的依赖时,Go 语言会在编译时报错:
[code]An import of a path containing the element “internal” is disallowed[/code]
if the importing code is outside the tree rooted at the parent of the
"internal" directory.
internal包不存在于当前项目树中才会发生,如果在同一个项目中引入该项目的
internal包并不会出现这种错误。/src
/src了,社区中的一些项目确实有
/src文件夹,但是这些项目的开发者之前大多数都有 Java 的编程经验,这在 Java 和其他语言中其实是一个比较常见的代码组织方式,但是作为一个 Go 语言的开发者,我们不应该允许项目中存在
/src目录。
$GOPATH/src目录下,这个目录中存储着我们开发和依赖的全部项目代码,如果我们在自己的项目中使用
/src目录,该项目的
PATH中就会出现两个
src:
[code]$GOPATH/src/github.com/draveness/project/src/code.go[/code]
/src目录的最重要原因。
/src目录也不会导致编译不通过或者其他问题,如果坚持这种做法对于项目的可用性也没有任何的影响,但是如果想让我们『看起来』更专业,还是遵循社区中既定的约定减少其他 Go 语言开发者的理解成本,这对于社区来说是一件好事。平铺
pkg目录结构的框架时,我们往往需要使用
github.com/draveness/project/pkg/somepkg,当代码都平铺在项目的根目录时只需要使用
github.com/draveness/project,很明显地减少了引用依赖包语句的长度。
/cmd目录中存储的都是当前项目中的可执行文件,该目录下的每一个子目录都应该包含我们希望有的可执行文件,如果我们的项目是一个
grpc服务的话,可能在
/cmd/server/main.go中就包含了启动服务进程的代码,编译后生成的可执行文件就是
server。
/cmd目录中放置太多的代码,我们应该将公有代码放置到
/pkg中并将私有代码放置到
/internal中并在
/cmd中引入这些包,保证
main函数中的代码尽可能简单和少。/api
/api目录中存放的就是当前项目对外提供的各种不同类型的 API 接口定义文件了,其中可能包含类似
/api/protobuf-spec、
/api/thrift-spec或者
/api/http-spec的目录,这些目录中包含了当前项目对外提供的和依赖的所有 API 文件:
[code]$ tree ./api[/code]
api
└── protobuf-spec
└── oceanbookpb
├── oceanbook.pb.go
└── oceanbook.proto
Makefile文件也非常值得被关注,在任何一个项目中都会存在一些需要运行的脚本,这些脚本文件应该被放到
/scripts目录中并由
Makefile触发,将这些经常需要运行的命令固化成脚本减少『祖传命令』的出现。小结
models、
views和
controllers,我们通过
rails new example生成一个新的 Rails 项目后可以看到其中的目录结构:
[code]$ tree -L 2 app[/code]
app
├── controllers
│ ├── application_controller.rb
│ └── concerns
├── models
│ ├── application_record.rb
│ └── concerns
└── views
└── layouts
model、
dao、
view的目录,这种按层拆分模块的设计其实有以下的几方面原因:
models、
views和
controllers的目录并按照层级的方式对模块进行拆分。按职责拆分
post、
user、
comment三个模块,每一个模块都对外提供相应的功能,
post模块中就包含相关的模型和视图定义以及用于处理 API 请求的控制器(或者服务):
[code]$ tree pkg[/code]
pkg
├── comment
├── post
│ ├── handler.go
│ └── post.go
└── user
import关键字引入相应的文件目录,再通过
pkg.xxx的形式引用其他目录定义的结构体、函数或者常量,如果我们在 Go 语言中使用
model、
view�和
controller来划分层级,你会在其他的模块中看到非常多的
model.Post、
model.Comment和
view.PostView。
package拆出去,对这部分性能热点单独进行扩容;
package中,这并不是说 Go 语言中不存在模块的水平拆分,只是因为
package作为一个 Go 语言访问控制的最小粒度,所以我们应该遵循顶层的设计使用这种方式构建高内聚的模块。显式与隐式
init为例,介绍 Go 语言社区对显式调用的推崇;相信很多人都在一些
package中阅读过这样的代码:
[code]var grpcClient *grpc.Client[/code]
func init() {
var err error
grpcClient, err = grpc.Dial(...)
if err != nil {
panic(err)
}
}
func GetPost(postID int64) (*Post, error) {
post, err := grpcClient.FindPost(context.Background(), &pb.FindPostRequest{PostID: postID})
if err != nil {
return nil, err
}
return post, nil
}
init函数其实隐式地初始化了 grpc 的连接资源,如果另一个
package依赖了当前的包,那么引入这个依赖的工程师可能会在遇到错误时非常困惑,因为在
init函数中做这种资源的初始化是非常耗时并且容易出现问题的。
Client结构体以及一个用于初始化结构的
NewClient函数,这个函数接收了一个 grpc 连接作为入参返回一个用于获取
Post资源的客户端,
GetPost成为了这个结构体的方法,每当我们调用
client.GetPost时都会用到结构体中保存的 grpc 连接:
[code]// pkg/post/client.go[/code]
type Client struct {
grpcClient *grpc.ClientConn
}
func NewClient(grpcClient *grpcClientConn) Client {
return &Client{
grpcClient: grpcClient,
}
}
func (c *Client) GetPost(postID int64) (*Post, error) {
post, err := c.grpcClient.FindPost(context.Background(), &pb.FindPostRequest{PostID: postID})
if err != nil {
return nil, err
}
return post, nil
}
main函数或者
main函数调用的其他函数中执行,如果我们在
main函数中显式的初始化这种依赖,对于其他的工程师来说就非常易于理解,我们从
main函数开始就能梳理出程序启动的整个过程。
[code]// cmd/grpc/main.go[/code]
func main() {
grpcClient, err := grpc.Dial(...)
if err != nil {
panic(err)
}
postClient := post.NewClient(grpcClient)
// ...
}
上图中出现的两个
Database其实是在main函数中初始化的数据库连接,在项目运行期间,它们可能表示同一个内存中的数据库连接
gochecknoinits和
gochecknoglobals静态检查时,它其实严格地限制我们对
init函数和全局变量的使用。
init函数,作为 Go 语言赋予开发者的能力,因为它能在包被引入时隐式地执行了一些代码,所以我们更应该慎重地使用它们。
init中判断是否满足使用的前置条件,但是对于很多的 Web 或者 API 服务来说,大量使用
init往往意味着代码质量的下降以及不合理的设计。
[code]func init() {[/code]
if user == "" {
log.Fatal("$USER not set")
}
if home == "" {
home = "/home/" + user
}
if gopath == "" {
gopath = home + "/go"
}
// gopath may be overridden by --gopath flag on command line.
flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}
init方法使用是展示的实例代码,这是一个比较合理地
init函数使用示例,我们不应该在
init中做过重的初始化逻辑,而是做一些简单、轻量的前置条件判断。error
if err != nil { return nil, err }的错误处理逻辑其实就是在显式地对错误处理,关注所有可能会发生错误的方法调用并在无法处理时抛给上层模块。
[code]func ListPosts(...) ([]Post, error) {[/code]
conn, err := gorm.Open(...)
if err != nil {
return []Post{}, err
}
var posts []Post
if err := conn.Find(&posts).Error; err != nil {
return []Post{}, err
}
return posts, nil
}
上述代码只是简单展示 Go 语言常见的错误处理逻辑,我们不应该在这种方法中初始化数据库的连接。
try/catch关键字,但是很少有人会在代码中使用
panic和
recover来实现错误和异常的处理,与
init函数一样,Go 语言对于
panic和
recover的使用也非常谨慎。
error实现错误处理 — 尽管这看起来非常啰嗦;
error也需要我们仔细地思考,向上抛出错误时可以通过
errors.Wrap携带一些额外的信息方便上层进行判断;
type ... interface的定义,那么作者可以推测出这在很大的概率上是一个工程质量堪忧并且没有多少单元测试覆盖的项目,我们确实需要认真考虑一下如何使用接口对项目进行重构。
单元测试是一个项目保证工程质量最有效并且投资回报率最高的方法之一,作为静态语言的 Golang,想要写出覆盖率足够(最少覆盖核心逻辑)的单元测试本身就比较困难,因为我们不能像动态语言一样随意修改函数和方法的行为,而接口就成了我们的救命稻草,写出抽象良好的接口并通过接口隔离依赖能够帮助我们有效地提升项目的质量和可测试性,我们会在下一节中详细介绍如何写单元测试。
[code]package post[/code]
var client *grpc.ClientConn
func init() {
var err error
client, err = grpc.Dial(...)
if err != nil {
panic(err)
}
}
func ListPosts() ([]*Post, error) {
posts, err := client.ListPosts(...)
if err != nil {
return []*Post{}, err
}
return posts, nil
}
init函数中隐式地初始化了 grpc 连接这种全局变量,而且没有将
ListPosts通过接口的方式暴露出去,这会让依赖
ListPosts的上层模块难以测试。
[code]package post[/code]
type Service interface {
ListPosts() ([]*Post, error)
}
type service struct {
conn *grpc.ClientConn
}
func NewService(conn *grpc.ClientConn) Service {
return &service{
conn: conn,
}
}
func (s *service) ListPosts() ([]*Post, error) {
posts, err := s.conn.ListPosts(...)
if err != nil {
return []*Post{}, err
}
return posts, nil
}
Service暴露对外的
ListPosts方法;
NewService函数初始化
Service接口的实现并通过私有的接口体
service持有 grpc 连接;
ListPosts不再依赖全局变量,而是依赖接口体
service持有的连接;
main函数中显式的初始化 grpc 连接、创建
Service接口的实现并调用
ListPosts方法:
[code]package main[/code]
import ...
func main() {
conn, err = grpc.Dial(...)
if err != nil {
panic(err)
}
svc := post.NewService(conn)
posts, err := svc.ListPosts()
if err != nil {
panic(err)
}
fmt.Println(posts)
}
Service对外暴露方法;
service实现接口中定义的方法;
NewService函数初始化
Service接口;
init函数、
error和接口,我们在这里主要是想通过三个不同的例子为大家传达的一个主要思想就是尽量使用显式的(explicit)的方式编写 Go 语言代码。单元测试
case开发的工程师可能已经不在团队中,而项目相关的文档可能也消失在了归档的
wiki中(更多的项目可能完全没有文档),我们能够在重构中相信的东西其实只有当前的代码逻辑(很可能是错误的)以及单元测试(很可能是没有的)。
Mock消灭不确定性,为了减少每一个单元测试的复杂度,我们需要:
Mock;
Mock。
[code]type Service interface { ... }[/code]
type service struct { ... }
func NewService(...) (Service, error) {
return &service{...}, nil
}
lint工具其实会对函数的理解复杂度(PerceivedComplexity)进行检查,也就是检查函数中出现的
if/else、
switch/case分支以及方法的调用的数量,一旦超过约定的阈值就会报错,Ruby 社区中的 Rubocop 和上面提到的 golangci-lint 都有这个功能。
10行,理解复杂度也不能超过
7,除此之外,Rubocop 其实还有其他的复杂度限制,例如循环复杂度(CyclomaticComplexity),这些复杂度的限制都是为了保证函数的简单和容易理解。组织方式
package进行组织的,
server.go文件对应的测试代码应该放在同一目录下的
server_test.go文件中。
_test.go结尾,当我们运行
go test ./pkg时就不会找到该文件中的测试用例,其中的代码也就不会被执行,这也是 Go 语言对于测试组织方法的一个约定。Test
_test.go结尾的文件中,所有的测试方法也都是以
Test开头并且只接受一个
testing.T类型的参数:
[code]func TestAuthor(t *testing.T) {[/code]
author := blog.Author()
assert.Equal(t, "draveness", author)
}
Add的方法写单元测试,那么对应的测试方法一般会被写成
TestAdd,为了同时测试多个分支的内容,我们可以通过以下的方式组织
Add函数相关的测试:
[code]func TestAdd(t *testing.T) {[/code]
assert.Equal(t, 5, Add(2, 3))
}
func TestAddWithNegativeNumber(t *testing.T) {
assert.Equal(t, -2, Add(-1, -1))
}
Test方法之外,我们可以使用
for循环来减少重复的测试代码,这在逻辑比较复杂的测试中会非常好用,能够减少大量的重复代码,不过也需要我们小心地进行设计:
[code]func TestAdd(t *testing.T) {[/code]
tests := []struct{
name string
first int64
second int64
expected int64
} {
{
name: "HappyPath":
first: 2,
second: 3,
expected: 5,
},
{
name: "NegativeNumber":
first: -1,
second: -1,
expected: -2,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, Add(test.first, test.second))
})
}
}
Add相关的测试分成一组方便我们进行观察和理解,不过这种测试组织方法需要我们保证测试代码的通用性,当函数依赖的上下文较多时往往需要我们写很多的
if/else条件判断语句影响我们对测试的快速理解。
suite包对测试进行组织:
[code]import ([/code]
"testing"
"github.com/stretchr/testify/suite"
)
type ExampleTestSuite struct {
suite.Suite
VariableThatShouldStartAtFive int
}
func (suite *ExampleTestSuite) SetupTest() {
suite.VariableThatShouldStartAtFive = 5
}
func (suite *ExampleTestSuite) TestExample() {
suite.Equal(suite.VariableThatShouldStartAtFive, 5)
}
func TestExampleTestSuite(t *testing.T) {
suite.Run(t, new(ExampleTestSuite))
}
suite包,以结构体的方式对测试簇进行组织,
suite提供的
SetupTest/
SetupSuite和
TearDownTest/
TearDownSuite是执行测试前后以及执行测试簇前后的钩子方法,我们能在其中完成一些共享资源的初始化,减少测试中的初始化代码。BDD
[code]var _ = Describe("Book", func() {[/code]
var (
book Book
err error
)
BeforeEach(func() {
book, err = NewBookFromJSON(`{
"title":"Les Miserables",
"author":"Victor Hugo",
"pages":1488
}`)
})
Describe("loading from JSON", func() {
Context("when the JSON fails to parse", func() {
BeforeEach(func() {
book, err = NewBookFromJSON(`{
"title":"Les Miserables",
"author":"Victor Hugo",
"pages":1488oops
}`)
})
It("should return the zero-value for the book", func() {
Expect(book).To(BeZero())
})
It("should error", func() {
Expect(err).To(HaveOccurred())
})
})
})
})
Describe、
Context以及
It等代码块,其中
Describe的作用是描述代码的独立行为、
Context是在一个独立行为中的多个不同上下文,最后的
It用于描述期望的行为,这些代码块最终都构成了类似『描述......,当......时,它应该......』的句式帮助我们快速地理解测试代码。Mock 方法
[code]package blog[/code]
type Post struct {}
type Blog interface {
ListPosts() []Post
}
type jekyll struct {}
func (b *jekyll) ListPosts() []Post {
return []Post{}
}
type wordpress struct{}
func (b *wordpress) ListPosts() []Post {
return []Post{}
}
jekyll或者
wordpress作为引擎,但是它们都会提供
ListsPosts方法用于返回全部的文章列表,在这时我们就需要定义一个
Post接口,接口要求遵循
Blog的结构体必须实现
ListPosts方法。
Blog接口之后,上层
Service就不再需要依赖某个具体的博客引擎实现了,只需要依赖
Blog接口就可以完成对文章的批量获取功能:
[code]package service[/code]
type Service interface {
ListPosts() ([]Post, error)
}
type service struct {
blog blog.Blog
}
func NewService(b blog.Blog) *Service {
return &service{
blog: b,
}
}
func (s *service) ListPosts() ([]Post, error) {
return s.blog.ListPosts(), nil
}
Service进行测试,我们就可以使用 gomock 提供的
mockgen工具命令生成
MockBlog结构体,使用如下所示的命令:
[code]$ mockgen -package=mblog -source=pkg/blog/blog.go > test/mocks/blog/blog.go[/code]
$ cat test/mocks/blog/blog.go
// Code generated by MockGen. DO NOT EDIT.
// Source: blog.go
// Package mblog is a generated GoMock package.
...
// NewMockBlog creates a new mock instance
func NewMockBlog(ctrl *gomock.Controller) *MockBlog {
mock := &MockBlog{ctrl: ctrl}
mock.recorder = &MockBlogMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockBlog) EXPECT() *MockBlogMockRecorder {
return m.recorder
}
// ListPosts mocks base method
func (m *MockBlog) ListPosts() []Post {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListPosts")
ret0, _ := ret[0].([]Post)
return ret0
}
// ListPosts indicates an expected call of ListPosts
func (mr *MockBlogMockRecorder) ListPosts() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPosts", reflect.TypeOf((*MockBlog)(nil).ListPosts))
}
mockgen生成的代码非常长的,所以我们只展示了其中的一部分,它的功能就是帮助我们验证任意接口的输入参数并且模拟接口的返回值;而在生成 Mock 实现的过程中,作者总结了一些可以分享的经验:
test/mocks目录中放置所有的 Mock 实现,子目录与接口所在文件的二级目录相同,在这里源文件的位置在
pkg/blog/blog.go,它的二级目录就是
blog/,所以对应的 Mock 实现会被生成到
test/mocks/blog/目录中;
package为
mxxx,默认的
mock_xxx看起来非常冗余,上述
blog包对应的 Mock 包也就是
mblog;
mockgen命令放置到
Makefile中的
mock下统一管理,减少祖传命令的出现;
[code]mock: rm -rf test/mocks mkdir -p test/mocks/blog mockgen -package=mblog -source=pkg/blog/blog.go > test/mocks/blog/blog.go
[/code]Service写单元测试了,这段代码通过
NewMockBlog生成一个
Blog接口的 Mock 实现,然后通过
EXPECT方法控制该实现会在调用
ListPosts时返回空的
Post数组:
[code][/code]
func TestListPosts(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockBlog := mblog.NewMockBlog(ctrl)
mockBlog.EXPECT().ListPosts().Return([]Post{})
service := NewService(mockBlog)
assert.Equal(t, []Post{}, service.ListPosts())
}
Service只依赖于
Blog的实现,所以在这时我们就能够断言当前方法一定会返回
[]Post{}
,这时我们的方法的返回值就只与传入的参数有关(虽然 ListPosts方法没有入参),我们能够减少一次关注的上下文并保证测试的稳定和可信。
package无论是项目内外都应该使用这种方式处理(在有接口的情况下),如果没有接口 Go 语言的单元测试就会非常难写,这也是为什么从项目中是否有接口就能判断工程质量的原因了。SQL
[code]func (s *suiteServerTester) TestRemovePost() {[/code]
entry := pb.Post{
Id: 1,
}
rows := sqlmock.NewRows([]string{"id", "author"}).AddRow(1, "draveness")
s.Mock.ExpectQuery(`SELECT (.+) FROM "posts"`).WillReturnRows(rows)
s.Mock.ExpectExec(`DELETE FROM "posts"`).
WithArgs(1).
WillReturnResult(sqlmock.NewResult(1, 1))
response, err := s.server.RemovePost(context.Background(), &entry)
s.NoError(err)
s.EqualValues(response, &entry)
s.NoError(s.Mock.ExpectationsWereMet())
}
ExpectQuery和
ExpectExec,前者主要用于模拟 SQL 的查询语句,后者用于模拟 SQL 的增删,从上面的实例中我们可以看到这个这两种方法的使用方式,建议各位先阅读相关的 文档 再尝试使用。HTTP
[code]func TestFetchArticles(t *testing.T) {[/code]
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder("GET", "https://api.mybiz.com/articles",
httpmock.NewStringResponder(200, `[{"id": 1, "name": "My Great Article"}]`))
httpmock.RegisterResponder("GET", `=~^https://api.mybiz.com/articles/id/d+z`,
httpmock.NewStringResponder(200, `{"id": 1, "name": "My Great Article"}`))
...
}
[code]func main() {[/code]
monkey.Patch(fmt.Println, func(a ...interface{}) (n int, err error) {
s := make([]interface{}, len(a))
for i, v := range a {
s[i] = strings.Replace(fmt.Sprint(v), "hell", "*bleep*", -1)
}
return fmt.Fprintln(os.Stdout, s...)
})
fmt.Println("what the hell?") // what the *bleep*?
}
rand.Int63n和
time.Now,编译器可能会直接将这种函数内联到调用实际发生的代码处并不会调用原有的方法,所以使用这种方式往往需要我们在测试时额外指定
-gcflags=-l禁止编译器的内联优化。
[code]$ go test -gcflags=-l ./...[/code]
interface或者修改
time.Now以及
rand.Int63n等内置函数的返回值用于测试时。
[code]func TestSomething(t *testing.T) {[/code]
assert.Equal(t, 123, 123, "they should be equal")
assert.NotEqual(t, 123, 456, "they should not be equal")
assert.Nil(t, object)
if assert.NotNil(t, object) {
assert.Equal(t, "Something", object.Value)
}
}
assert的示例,更详细的内容可以阅读它的相关文档,在这里也就不多做展示了。小结
model、
controller这种违反语言顶层设计思路的包名;
init函数,保证显式地进行方法的调用以及错误的处理;
Service对外暴露方法;
service实现接口中定义的方法;
func NewService(...) (Service, error)函数初始化
Service接口;
suite或者 BDD 的风格对单元测试进行合理组织;
转载自简书
本文为 @ 21CTO 创作并授权 21CTO 发布,未经许可,请勿转载。
内容授权事宜请您联系 webmaster@21cto.com或关注 21CTO 公众号。
该文观点仅代表作者本人,21CTO 平台仅提供信息存储空间服务。