- Writing in the front
- Code specification
- Auxiliary tool
- automation
- Auxiliary tool
- Best practices
- The directory structure
- Resolution of the module
- Explicit and implicit
- An interface
- summary
- The directory structure
- Unit testing
- testable
- organization
- The Mock method
- assertions
- summary
- testable
- conclusion
- Reference
Go language is a simple programming language that is easy to learn. For engineers with programming background, it is not difficult to learn Go language and write codes that can run. For developers with previous experience in other languages, it is actually problematic to write any language like the one they have learned. To truly embrace ecology and write elegant code, it’s important to spend some time and effort understanding the design philosophy and best practices behind the language.
If you have no previous Go development experience and are learning and using Go, this article will help you write elegant Go code faster. In this article, we are not going to give a long list of variables, methods, and structures to name. The Code specifications for the Go language can be found in the Go Code Review Comments section. They are important but not the focus of this article. We’ll show you how to write elegant Go code from different aspects of code structure, best practices, and unit testing.
Writing in the front
Writing good code is not easy, it requires constant rethinking of existing code — how to rewrite it to make it more elegant. Elegance sounds like a very emotional, hard-to-quantify result, but it’s the most intuitive thing good code can do, and it may implicitly include the following characteristics:
- Easy to read and understand;
- Easy to test, maintain, and extend;
- Clear naming, no ambiguity, perfect and clear annotation;
- …
By the end of this article, we won’t be able to write elegant Go code right away, but if we follow a few easy and practical methods, it will help us get started. The author wrote this article with the following goals:
- Help Go developers understand the specifications and tools in the ecosystem to write more elegant code;
- Provide community-wide rules, consensus, and best practices for code and project management;
Code specification
In fact, Code specification is an old and normal problem, we can not avoid the vulgar or simply introduce the relevant content, Go language is relatively common and widely used Code specification is the official Go Code Review Comments, whether you use Go language for short-term or long-term programming, This official code specification guide should be read at least once, both as a rule to follow when writing code and as a code review guideline.
Learning the language related code specification is a very important thing, is to make our project follows the first step in the unified specification, while reading the code specifications related documents is very important, but in practice we can’t rely on the engineer consciously abide by, and is often as a form of code review, but need to use tools to assist in execution.
Auxiliary tool
Using automated tools to ensure that project to comply with some of the most basic code specification is very easy to operate and effective, compared with human flesh review code way more easy to get wrong, also can appear some special case of the violation of rules and conventions, the best way to maintain code standard is “try to automate everything can be automated steps, Let the engineers review the logic and design that really matters.”
In this section, we’ll look at two very effective ways to automate some code specification checks and static checks on projects to ensure quality.
goimports
Goimports is an official Go tool that automatically formats Go code and manages all incoming packages, including automatically adding and removing dependent package references, alphabetizing and sorting dependent packages. Most ides use another official tool, Gofmt, to format code. Goimports is equivalent to GoFMT plus dependency package management.
All Go developers are advised to use GoImports while developing. Although GoImports sometimes introduces incorrect packages, these occasional errors are acceptable to the authors compared to the benefits they bring; Of course, developers who don’t want to use Goimports must also enable automatic GofMT (automatic formatting when saving) in the IDE or editor.
It is not and should not be discussed to enable automatic GOfMT or Goimports checks in IDE and CI checks, it is just a must to use and develop the Go language.
golint
Another popular static check tool is Golint. As an official tool, it has very poor support for customizability. We can only check our projects by running Golint in the following way:
$ golint ./pkg/...
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
...
Copy the code
There is a discussion in the community about golint customization, and golint developers give the following points of view to explain why Golint does not support customization:
lint
The goal of the Go language community is to encourage a common, consistent programming style. Some developers may not agree with some of the specifications, but the Go language community has a strong advantage in using a common style, and the ability to turn rules on and off can lead togolint
Unable to do the job effectively;- There are some statically checked rules that result in error warnings, which can be quite annoying, but I would prefer to support keeping or removing these rules in Golint rather than providing the ability to add or remove rules at will;
- Can pass
min_confidence
Filter some static check rules, but we need to select the appropriate values;
The author’s opinion of Golint gets a lot of 👎 in the issue, but it is hard to say right or wrong about this matter; Having a consistent programming specification in the community is a very good thing, but for many intra-company services or projects, where there may be some tricky situations on business services, there is not much obvious benefit to using such a strong constraint.
It is more recommended to use Golint for static checking in base libraries or frameworks (or both golint and Golangci-Lint) and customizable Golangci-Lint for static checking in other projects. Because enforcing restrictions in the base library and framework has greater benefits for overall code quality.
The author will use golint + Golangci-Lint in his Go project and turn on all checks to find bugs in the code, including documentation, as soon as possible.
automation
Whether it’s goimports for checking code specifications and dependencies, or the static checking tools glint or Golangci-Lint, whenever we introduce these tools into a project, it’s important to include the corresponding automated checking in the CI flow of the code:
- On GitHub we can use Travis CI or CircleCI;
- On Gitlab we can use Gitlab CI;
You should also try to find the right tools on your own or other code hosting platforms. Modern code hosting tools should support CI/CD very well. We need to use these CI tools to make automated code reviews a prerequisite for PR merges and releases, reducing the potential for leaks when engineers Review code.
Best practices
In the previous section, we introduced some of the problems that can be detected by automated tools. The best practices mentioned in this section may not be guaranteed by automated tools, but are more like the engineering experience and consensus accumulated during the development of the Go language community. Following these best practices can help us write code that fits the “taste” of the Go language, and we’ll cover the following in this section:
- Directory structure;
- Module splitting;
- Explicit call;
- Interface oriented;
These four parts are relatively common conventions in the community. If we learn and follow these conventions and put them into practice in Go language projects, it will certainly be helpful for us to design Go language projects.
The directory structure
The directory structure is basically the facade of a project, and it’s often a good indicator of how experienced the developer is with the language, so the first best practice here is how to organize code in a Go project or service.
There is no official recommendation for directory division, and many projects are quite arbitrary about directory structure division, which is not a problem, but there are some common conventions in the community, such as: A standard directory structure is defined in the Golang-standards/project-Layout project.
School Exercises ─ LICENSE. Md School Exercises ─ Makefile School Exercises ─ README. Md School Exercises ─ API School Exercises ─ Assets School Exercises ─ Build School Exercises ─ CMD School Exercises ─ deployments School Exercises ─ docs School Exercises ─ Examples ├ ─ ─ githooks ├ ─ ─ init ├ ─ ─ internal ├ ─ ─ PKG ├ ─ ─ scripts ├ ─ ─ the test ├ ─ ─ third_party ├ ─ ─ the tools ├ ─ ─ vendor ├ ─ ─ the web └ ─ ─ websiteCopy the code
Here we will briefly introduce a few of the more common and important directories and files to help us quickly understand how to use the directory structure shown above. If you want to understand the reasons for using other directories, You can learn more from README in the Golang-standards/project-Layout project.
/pkg
The/PKG directory is a very common directory in Go language projects, and can be found in almost all well-known open source projects (non-frameworks), such as:
- Prometheus a timing database for reporting and storing metrics
- Istio Service Grid 2.0
- Kubernetes container scheduling management system
- Grafana displays a dashboard of monitoring and metrics
This directory contains code libraries from the project that can be used by external applications. Other projects can import code directly from this directory, so we should be careful when putting code into the PKG. However, if we are developing HTTP or RPC interface services or internal company services, Will be in the private and public code in/PKG nor too many wrong, because as the top projects rarely by other applications rely on directly, of course strictly follow the public and private code division is a very good practice, the author also advises you to project developers in the division of public and private code properly.
Private code
Private code is recommended to be placed in the /internal directory. Real project code should be written in /internal/app, and the code base that these internal applications depend on should be in the /internal/ PKG subdirectory and/PKG. The following figure shows the structure of a project using the /internal directory:
When we introduce dependencies that contain internal in other projects, Go will report an error when compiling:
An import of a path containing the element "internal" is disallowed if the importing code is outside the tree crcrat the parent of the "internal" directory.Copy the code
This error occurs only if the imported internal package does not exist in the current project tree, and does not occur if the internal package of the project is introduced in the same project.
/src
Some projects in the community do have/SRC folders, but most of the developers of these projects have prior Java programming experience. This is actually a common way to organize code in Java and other languages. But as a developer of the Go language, we should not have allowed the/SRC directory to exist in our projects.
The most important reason is that by default, Go projects are placed in the $GOPATH/ SRC directory, which stores all of the project code we developed and relied on. If we use the/SRC directory in our own project, the PATH of the project will have two SRC:
$GOPATH/src/github.com/draveness/project/src/code.go
Copy the code
The directory structure above looks very strange, which is the most important reason we don’t recommend using the/SRC directory in Go.
, of course, even if we in the language used in the project/SRC directory and not lead to compile by or other problems, if you insist on this kind of practice for usability, does not have any impact of the project, but if we want to “look” more professional, or follow the established conventions of the community to reduce other Go language developers understand the cost, It’s a good thing for the community.
tile
Another way to organize code in Go is to place the project’s code in the project’s root directory. This is common in many frameworks or libraries. If you want to introduce a framework that uses the PKG directory structure, We often need to use github.com/draveness/project/pkg/somepkg, when code is tile in the root directory of the project only need to use github.com/draveness/project, obviously reduce the length of the referenced package statement.
So for a Go framework or library, tiling code in the root directory is normal, but in a Go service this kind of code organization might not be appropriate.
/cmd
The/CMD directory stores the executable files in the current project. Each subdirectory in this directory should contain the executable files we want. If our project is a GRPC service, it might contain the code to start the service process in/CMD /server/main.go. The compiled executable is the Server.
Instead of putting too much code in the/CMD directory, we should put the public code in/PKG and the private code in /internal and introduce these packages in/CMD, keeping the code in main as simple and as minimal as possible.
/api
The/API directory stores various types of API definition files provided by the current project. These may contain directories such as/API /protobuf-spec, / API /thrift-spec, or/API /http-spec. These directories contain all API files that the current project provides and relies on:
$imp./ API API ├─ ├─ oceanbook.pt. go ├─ oceanbook.protoCopy the code
The primary purpose of a secondary directory is to avoid potential conflicts when a project provides multiple access methods at the same time, and to make the organization of the project structure clearer.
Makefile
In any project, there will be scripts that need to be run. These scripts should be placed in the /scripts directory and triggered by the Makefile to solidify these frequently run commands into scripts to reduce the occurrence of “inherited commands”.
summary
In general, each project should be implemented in a fixed organizational way. Although this agreement is not mandatory, it is very helpful for other engineers to quickly comb through and understand the project, whether within the group, the company or the entire Go language community, once it is agreed upon.
The way Go projects are organized in this section is not mandatory, it’s just the way they are often organized in the Go community, and it can be tweaked for a larger project, but it’s more common and reasonable.
Resolution of the module
Now that we’ve covered how to organize the structure of a project from the top, we’ll dive into the internals of the project and introduce some of the ways that Go splits modules.
Some of the top-level design of Go ultimately makes it very different from other programming languages in terms of module partitioning. Many other Web frameworks use MVC architectural patterns, such as Rails and Spring MVC, Go has a completely different approach to module partitioning than Ruby and Java.
According to the layer separation
The most famous frameworks, both Java and Ruby, are heavily influenced by the MVC architectural pattern, which we can see from the name Spring MVC, and the Rails framework in the Ruby community is also very closely related to MVC. This is the most common way of architecting a Web framework, dividing the different components of a service into three layers: Model, View, and Controller.
Rails scaffolding generates code by default to place the three different layers of source files in their respective directories: We can see the directory structure of a New Rails project by generating models, views, and controllers:
$├─ ├─ exercises, exercises, exercises, exercises, exercises, exercises, exercises, exercises, exercises, exercises ├ ─ garbage ├ ─ garbage │ ├ ─ layoutsCopy the code
Many Spring MVC projects also have directories like Model, DAO and View. There are several reasons for this design:
- MVC architecture pattern – MVC itself emphasizes the design of dividing responsibilities by layers, so the framework that follows this pattern naturally has the same line of thought;
- Flat namespaces – Whether Spring MVC or Rails, namespaces in the same project are very flat, and there is no need to introduce new packages to use classes or methods defined in other folders across folders, and there is no need to add extra prefixes to use classes defined in other files. Classes defined by multiple files are “merged” into the same namespace;
- Single service scenarios – When Spring MVC and Rails first emerged, SOA and microservice architectures were not as common as they are today, and the vast majority of scenarios did not require the separation of services;
Several of the reasons above combine to make Spring MVC and Rails have directories for Models, Views, and Controllers and split modules hierarchically.
Split by duties
Go takes a completely different approach to module splitting. Although the MVC architectural pattern is unavoidable when we write Web services, Go projects tend to split modules by responsibility rather than horizontally shred different layers:
For a common blog system, the project using Go language will be vertically divided into post, User and comment modules according to different responsibilities, and each module provides corresponding functions externally. The POST module contains the associated model and view definitions and the controller (or service) used to handle API requests:
$tree ├── comment ├─ post │ ├─ hand.go ├── postCopy the code
Each file directory of the Go language programs represent a separate namespace, which is a separate package, when we want to refer to other folder directory, you first need to use the import keywords into the corresponding file directory, again through the PKG. XXX in the form of a reference other directory defines the structure, function, or constant, If we use model, View, and Controller hierarchies in Go, you’ll see a lot of Model.post, model.ment, and View.postview in other modules.
This hierarchical approach can be very redundant in Go, and reference loops can easily occur if project dependencies are not carefully managed. The root cause of these problems is very simple:
- Go language isolates the namespaces of different directories in the same project. Classes and methods defined in the whole project are not in the same namespace, which requires engineers to maintain the dependency between different packages.
- It is very easy to split microservices according to the vertical separation of responsibilities when individual services encounter bottlenecks. We can directly split one responsible for independent functions
package
Remove them and expand the capacity of these performance hotspots separately.
summary
There is no absolute good or bad in terms of whether a project breaks down modules by hierarchy or by responsibility; design at the language and framework levels ultimately determines how projects and code should be organized.
Languages such as Java and Ruby tend to divide responsibilities at different levels in frameworks by horizontal splitting. However, the best practice of Go projects is to vertically split modules according to responsibilities and divide code into multiple packages according to functions. This is not to say that horizontal splitting of modules does not exist in Go. Just because package is the least granular access control of a Go language, we should follow the top-level design and use this approach to build highly cohesive modules.
Explicit and implicit
From learning and using Go to participating in open source Golang projects in the community, the author found that explicit initialization, method calls, and error handling were highly appreciated by the Go community, and that frameworks like Spring Boot and Rails widely adopted the central idea of convention over configuration. Simplifies the workload of developers and engineers.
While there is a lot of consensus and convention in the Go language community, explicit method calls and error handling are encouraged by language design and tool use.
init
Let’s start with a very common init function as an example of the Go language community’s preference for explicit calls. I’m sure many of you have read code like this in some packages:
var grpcClient *grpc.Client 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 }Copy the code
While this code compiles and works, init implicitly initializes the GRPC connection resource. If another package relies on the current package, the engineer introducing this dependency might get confused when he encounters an error. This is because doing this resource initialization in the init function is time-consuming and problematic.
A more logical approach would be to define a NewClient structure and a NewClient function to initialize the structure. This function takes a GRPC connection as an input parameter and returns a Client to fetch the Post resource. GetPost becomes the method of this structure, and we use the GRPC connection stored in the structure whenever we call client.getPost:
// pkg/post/client.go 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 }Copy the code
The code that initializes the GRPC connection should be placed in main or any other function called by Main. If we explicitly initialize this dependency in main, it will be easy for other engineers to understand. We can tease out the process of starting the program from main.
// cmd/grpc/main.go func main() { grpcClient, err := grpc.Dial(...) if err ! = nil { panic(err) } postClient := post.NewClient(grpcClient) // ... }Copy the code
Each module will form a tree structure and dependency relationship, the upper module will hold the interface or structure in the lower module, there will be no isolated and unreferenced objects.
The two databases that appear in the figure above are actually Database connections initialized in the main function and may represent the same in-memory Database connection during the project run
When we use golangci-Lint and turn on goChecknoinits and GoChecknoGlobals static checks, it severely restricts our use of init functions and global variables.
Of course, this is not to say that we should not use the init function, which is given to developers by the Go language, because it implicitly executes some code when packages are introduced, so we should use them with caution.
Some frameworks use init to determine if the preconditions are met, but for many Web or API services, heavy use of init often means poor code quality and poor design.
func init() {
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")
}
Copy the code
This is a good example of how to use the Effective Go init function. Instead of doing too much initialization logic in init, we should do some simple, lightweight preconditions.
error
Golang’s error handling has been criticized by developers for a long time, but engineers write if Err! The error-handling logic of = nil {return nil, err} is essentially an explicit error-handling logic, focusing on all possible method calls that could go wrong and throwing it to the upper module if it can’t handle it.
func ListPosts(...) ([]Post, error) { 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 }Copy the code
The above code simply shows common error handling logic in Go, and we should not initialize a database connection in this way.
Although there are similar Java or Ruby try/catch keywords in Golang, few people use panic and Recover in their code to handle errors and exceptions, as in init, The Go language is also cautious about using Panic and Recover.
When dealing with error-related logic in Go, the most important things are the following:
- use
error
Implement error handling — even though this seems very verbose; - Throw the error to upper-level processing– Whether a return is required for a method
error
It also requires us to think carefully and throw up errors when we can passerrors.Wrap
Carry some additional information for superior judgment; - Handle any errors that may return – all errors that may return will eventually return, considering all aspects helps us build more robust projects;
summary
During my time with Go, I was able to appreciate its encouragement of explicit method calls and error handling, which not only helped other developers on the project quickly understand context, but also helped us build projects that were more robust, fault-tolerant, and maintainable.
An interface
Interface oriented programming is an old topic. The function of interface is to provide a defined middle layer for modules of different levels. Upstream does not need to rely on the concrete implementation of downstream, and fully decouples upstream and downstream.
This approach to programming is recommended not only in Go, but in almost any programming language. It provides a great deal of flexibility for our programs, and it is impossible to build a stable, robust Go project without using an interface.
If a moderately sized project doesn’t have any type in it… Then the authors can speculate that this is a project with a high probability of poor engineering quality and little unit test coverage, and that we really need to think hard about how to refactor the project using interfaces.
Unit testing is a project to ensure the engineering quality is one of the most effective and highest return on investment method, as a static language Golang, want to write coverage enough (at least to cover the core logic) unit test itself is difficult, because we can’t revise functions and methods as a dynamic language behavior, and the interface is our lifeline, Writing well-abstracting interfaces and isolating dependencies through interfaces can help improve the quality and testability of projects, as we’ll cover in more detail how to write unit tests in the next section.
Package POST 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 }Copy the code
Not only does it implicitly initialize the global variable GRPC connection in the init function, but it doesn’t expose ListPosts through the interface, which makes it difficult for the upper modules that rely on ListPosts to test.
We can rewrite the logic to make the same logic easier to test and maintain with the following code:
package post 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 }Copy the code
- Through the interface
Service
exposedListPosts
Methods; - use
NewService
Function initializationService
The implementation of an interface and through a private interface bodyservice
Hold GRPC connection; -
ListPosts
Instead of relying on global variables, you rely on the interface bodyservice
Hold the connection;
After refactoring the code in this way, we can explicitly initialize the GRPC connection in the main function, create an implementation of the Service interface, and call the ListPosts method:
package main 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) }Copy the code
This way of organizing code using interfaces is very common in Go, and we should use this idea and pattern as much as possible in our code to provide functionality externally:
- Capitalized
Service
External exposure method; - Use lowercase
service
Implement the methods defined in the interface; - through
NewService
Function initializationService
Interface;
When we organize our code in this way, we can actually decouple the dependencies of different modules and follow the oft-cited phrase in software design – “rely on interfaces, not implementations”, that is, interface oriented programming.
summary
In this section we introduce three common Go “elements” — the init function, error, and interface. The main idea we want to convey through three different examples is to make Go code as explicit as possible.
Unit testing
A code quality and project quality assured project must have a reasonable unit test coverage, there is no unit test project must be not qualified or not important, the unit test should be all projects must be some code, each unit test indicates a possible situation, unit testing is the business logic.
As software engineers, it is quite normal for us to reconstruct the existing project. If there is no unit test in the project, it is difficult for us to reconstruct the project without changing the existing business logic, and some business boundary conditions are likely to be lost in the process of reconstruction. Was involved in the corresponding case development engineer might have not in the team, and a project related document may disappear in the wiki archive (more projects may no documents), we can believe in refactoring actually it is only the current code logic is wrong (probably) and unit test (probably not).
Simple to summarize, the lack of a unit test will not only means that the low engineering quality, and the means to reconstruct, a unit test project and will not be able to ensure the completely same before and after the reconstruction of logic, without a unit test project quality of the project is likely to itself is worrying, not to mention how to under the condition of without losing business logic reconstruction.
testable
Writing code is not that difficult, but it is not easy to write testable code in a project, and elegant code must be testable. What we need to discuss in this section is what kind of code is testable.
If we want to figure out what is testable, we first need to know what is a test? The author’s understanding of testing is that control variables, after we isolate some of the dependencies in the method under test, should get the expected return value when the function’s input parameters are determined.
How to control the dependencies of the method to be tested is crucial when writing unit tests. Controlling dependencies means mocking the dependencies of the target function to eliminate uncertainty. To reduce the complexity of each unit test, we need to:
- Reduce the dependency of the target method as much as possible, so that the target method only depends on the necessary modules;
- Dependent modules should also be very easy to implement
Mock
;
Unit test execution should not depend on any external module, whether it is to call external HTTP requests or data in the database, we should try to simulate the possible situation, because the unit test is not integration test, it should not rely on any other system except the project code.
interface
In Go, we can’t write code that is easy to test if we don’t use interfaces at all. Golang is a static language, and only when we use interfaces can we get out of the trap of relying on concrete implementations. Interfaces can give us clearer abstractions and help us think about how to design code. It also makes it easier to Mock out dependencies.
Let’s review the common patterns shown in the introduction to interfaces in the previous section:
type Service interface { ... } type service struct { ... } func NewService(...) (Service, error) { return &service{... }, nil }Copy the code
The above code is very common in the Go language, if you do not know whether to use the interface to provide services, then it is a good time to use the above mode of external exposure method, this mode works in the vast majority of scenarios, at least the authors have not seen not applicable.
Simple function
Another tip is to keep each function as simple as possible, and by simple I mean not only simple and unitary in function, but also easy to understand and self-explanatory in naming functions.
Some languages’ lint tools check functions’ PerceivedComplexity, that is, the number of if/else, switch/case branches, and method calls, and raise an error when their functions are perceived by some conventions. Rubocop in the Ruby community and the aforementioned Golangci-Lint have this feature.
Rubocop in the Ruby community has very strict limits on the length and complexity of functions. By default, functions cannot exceed 10 lines and the complexity of functions cannot exceed 7. In addition, Rubocop has other complexity limits. For example, CyclomaticComplexity is limited to keep the function simple and easy to understand.
organization
It is also worth discussing how to organize tests. The unit test files and code in Golang are organized according to package in the same directory as the source code, and the test code corresponding to the server.go file should be placed in the same directory as the server_test.go file.
If the file does not end with _test.go, the test cases in the file will not be found when we run go test. / PKG and the code in the file will not be executed, which is a convention of the GO language for test organization methods.
Test
The most common and default way to organize unit tests is to write them in a file ending with _test.go. All Test methods also start with Test and accept only one parameter of type testing.t:
func TestAuthor(t *testing.T) {
author := blog.Author()
assert.Equal(t, "draveness", author)
}
Copy the code
If we were to write unit tests for a method named Add, the corresponding test method would be written as TestAdd. To test the contents of multiple branches at the same time, we could organize our Add tests in the following way:
func TestAdd(t *testing.T) {
assert.Equal(t, 5, Add(2, 3))
}
func TestAddWithNegativeNumber(t *testing.T) {
assert.Equal(t, -2, Add(-1, -1))
}
Copy the code
In addition to spreading a function-related Test over multiple Test methods, we can use a for loop to reduce duplicate Test code. This is useful in logically complex tests and can reduce the amount of duplicate code, but it also needs to be carefully designed:
func TestAdd(t *testing.T) {
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))
})
}
}
Copy the code
This way also can actually generate test results, the tree will Add related test is divided into a set of convenient us to observe and understand, but we can assure you that this method of testing organizations need to test the generality of the code, when function based on the context of there are most often require us to write a lot of conditions of the if/else statement affect our quick understanding of the test.
Authors often use the first organization when the test code is simple, and the second when there are more dependencies and more complex functions, but this is not the last word, and we need to decide how to design our tests based on the actual situation.
Suite
The second most common way to organize tests is by cluster, which is a simple encapsulation of the default test method of the Go language. We can use the Stretchr/Suite package to organize tests:
import ( "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)) }Copy the code
We can use the suite package, clusters of the test in the form of structure to organize, suite SetupTest/SetupSuite and TearDownTest/TearDownSuite is performed before and after the test, and execute the test cluster method before and after the hook, We were able to do some initialization of shared resources there, reducing the initialization code in the test.
BDD
The final way to organize code is to organize unit tests in the BDD style. Ginkgo is the most common BDD framework in the Golang community. Behavior Driven Development (BDD) and test driven development (TDD) are both methodologies for ensuring project quality. To put this idea into practice in a project, we need to change our thinking and adapt to it. That is, we need to write the Spec of the unit test or behavior test convention method first, and then implement the method to make our test pass. This is a scientific method, which can give us a strong confidence.
We don’t have to use BDD/TDD thinking to develop our projects, but we can use BDD style to organize very readable test code:
var _ = Describe("Book", func() {
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())
})
})
})
})
Copy the code
BDD frameworks generally contain code blocks such as Describe, Context, and It, where Describe describes the independent behavior of the code, Context is multiple different contexts within a separate behavior, and It describes the desired behavior. These blocks of code eventually form something like “Describe… when… it should…” “Helps us quickly understand the test code.
The Mock method
The unit test in your project should be stable and not dependent on any external project. It only tests the functions and methods in your project, so we need to Mock all the unstable dependencies of third parties in our unit test, that is, to simulate the interfaces of these third party services. In addition, to simplify the context of a unit test, we Mock other modules in the same project to simulate the return values of those dependent modules.
Unit testing is all about isolating dependencies and verifying the correctness of inputs and outputs. As a static language, Go offers few runtime features, which makes it difficult to Mock dependencies in Go.
The main purpose of a Mock is to ensure that the context in which the method is being tested depends is fixed. At this point, no matter how many times we run unit tests on the current method, if the business logic does not change, it should return exactly the same result. Before going into the different methods of a Mock, we need to be aware of some common dependencies. Common dependencies on a function or method can be the following:
- interface
- The database
- The HTTP request
- Redis, caching, and other dependencies
These different scenarios basically cover what happens when you write unit tests, and we’ll describe how to handle each of these dependencies in the following sections.
interface
The most common and common Mock method in Go is the Golang/Mock framework that mocks an interface. It generates a Mock implementation based on the interface, assuming we have the following code:
package blog
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{}
}
Copy the code
Our blog might use Jekyll or wordpress as an engine, but both provide ListsPosts to return a full list of posts, at which point we need to define a Post interface, The interface requires that a structure that follows a Blog must implement the ListPosts method.
Once we have defined the Blog interface, the upper layer Service no longer needs to rely on a specific Blog engine to implement, but only rely on the Blog interface to complete the bulk fetching of articles:
package service
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
}
Copy the code
If we wanted to test the Service, we could generate a MockBlog structure using the Mockgen tool command provided by Gomock, using the following command:
$ mockgen -package=mblog -source=pkg/blog/blog.go > test/mocks/blog/blog.go $ 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)) }Copy the code
This mockgen generated code is very long, so we only show a portion of it. Its function is to help us validate the input parameters of any interface and simulate the return values of the interface. While generating Mock implementations, the authors have some lessons to share:
- in
test/mocks
Place all Mock implementations in the same subdirectory as the level 2 of the interface file, where the source file is located inpkg/blog/blog.go
, its secondary directory isblog/
, so the corresponding Mock implementation is generated totest/mocks/blog/
Directory; - The specified
package
为mxxx
, the defaultmock_xxx
Looks very redundant aboveblog
The Mock package corresponding to the package ismblog
; -
The mockgen command is placed in the Makefile and managed uniformly under the mock to reduce the occurrence of ancestral commands.
The mock: rm -rf test/mocks mkdir -p test/mocks/blog mockgen -package=mblog -source=pkg/blog/blog.go > test/mocks/blog/blog.goCopy the code
Once we have generated the Mock implementation code above, we can write unit tests for the Service as follows: This code generates a Mock implementation of the Blog interface through NewMockBlog, Controlling that implementation with the EXPECT method then returns an empty ARRAY of POSTS when ListPosts is called:
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())
}
Copy the code
Since the current Service only relies on the Blog implementation, at this point we can assert that the current method must return []Post{}, and our method’s return value is only relevant to the argument passed in (although the ListPosts method has no input arguments). We are able to reduce the context of concern at a time and keep the tests stable and reliable.
This is the standard unit test in the language writing, all depend on the package both inside and outside the project should use this way to deal with (in the case of the interface), if there is no interface Go language unit tests will be very difficult to write, which is why from the project if there were any interface can determine the cause of the engineering quality.
SQL
Another common dependency in a project is a database. When we encounter a database dependency, we will use SQLMock to simulate the connection to the database. When we use SQLMock, we will write the following unit test:
func (s *suiteServerTester) TestRemovePost() {
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())
}
Copy the code
The most commonly used methods are ExpectQuery and ExpectExec. The former is mainly used to simulate SQL queries, and the latter is used to simulate SQL additions and deletions. From the above examples, we can see how to use these two methods.
HTTP
HTTP requests are also common dependencies in projects. Httpmock is a package that mocks all HTTP dependencies. It matches the URL of an HTTP request using pattern matching and returns a preset response when a specific request is matched.
func TestFetchArticles(t *testing.T) {
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"}`))
...
}
Copy the code
If you encounter a dependency on an HTTP request, you can use the httpMock package above to simulate the dependent HTTP request.
The monkey patch
The bouk/ Monkey can change the implementation of any function by replacing the pointer to the function, so if none of the above methods satisfy our needs, we can only rely on the monkey patch method as a hack:
func main() { 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*? }Copy the code
However, there are some limitations to using this method. Because it replaces a pointer to a function at run time, the compiler may inline simple functions such as rand.int63n and time.now directly to the code where the call actually took place instead of calling the original method. So using this approach often requires us to specify -gcflags=-l to disable the compiler’s inline optimization during testing.
$ go test -gcflags=-l ./...
Copy the code
The Bouk/Monkey README has some caveats for its use. In addition to inline compilation, we should be careful not to use monkey patches outside of unit tests. We should only use this method when necessary. For example, when a dependent third-party library does not provide interface or modify the return value of built-in functions such as time.Now and rand.int63n for testing purposes.
In theory, monkey patches enable all functions in the Mock Go language to run at runtime, which provides the ultimate solution for unit testing Mock dependencies.
assertions
Finally, let’s take a quick look at the Assert package for unit testing, which provides a number of assertion methods to help us quickly test the expected return value and reduce our workload:
func TestSomething(t *testing.T) {
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)
}
}
Copy the code
Here, we’ll briefly show you an example of Assert, but you can read its documentation for more details.
summary
If no written before the unit test or didn’t write the language unit tests, believe that this article has given enough context help we began to do this, we will know is a unit test tool will not hinder our progress, it can provide confidence to our online, is also a way of quality assurance on the highest return on investment.
Learning to write well unit tests is bound to have some learning curve and discomfort, which may even affect our development efficiency in the short term. However, once we are familiar with this set of processes and interfaces, unit tests can be very helpful. Each unit test represents a business logic. Performing unit tests on each commit helped us determine that the new code would most likely not affect the existing business logic, significantly reducing the risk of refactoring and the number of online incidents
conclusion
In this article, we introduce how to write elegant Go code from three aspects, and the author gives the easiest and most efficient method possible:
- Code specification: Use tools to automate code review every time we submit a PR, reducing the amount of manual review by engineers;
- Best practices
- Directory structure: Follow the widely agreed directory structure in the Go language community to reduce project communication costs;
- Module splitting: Splitting different modules according to their responsibilities should not occur in Go projects
model
,controller
Package names that violate language top-level design ideas; - Explicit and implicit: Eliminate as many items as possible
init
Function, ensuring explicit method calls and error handling; - Interface oriented: Interface oriented development is encouraged by THE Go language. It also provides convenience for us to write unit tests. We should follow a fixed pattern to provide external functions.
- Capitalized
Service
External exposure method; - Use lowercase
service
Implement the methods defined in the interface; - through
func NewService(...) (Service, error)
Function initializationService
Interface;
- Capitalized
- Unit testing: the most effective way to ensure the quality of the project;
- Testable: means interface oriented programming and reducing the logic contained in individual functions, using “small methods”;
- Organization: Use the default Test framework of Go language, open source
suite
Or BDD style organization of unit tests; - Mock methods: Four different unit test Mock methods;
- Gomock: The most standard and most encouraged way;
- Sqlmock: Handles dependent databases;
- Httpmock: Handles dependent HTTP requests;
- Monkey: a way to do everything, but only when you have to. Code like this is tedious and unintuitive.
- Assert: Use the community’s Potency to quickly verify the return value of the potency method
Want to write elegant code itself is not an easy thing, it requires us to constantly update their knowledge system and optimization, pulled down before the experience and the project continued to improve and reconstruct, and only through real thinking and design code to pass the test of time (need to constantly refactoring code), Random piling of code is not encouraged and should not happen. Every line of code should be designed and developed to the highest standards. This is the only way we can guarantee the quality of the project.
The author has been trying to learn how to write more elegant code. It is not easy to write good code. The author also hopes that this article will help engineers who use Go to write more Golang-style projects.
Reference
- goimports vs gofmt
- Style guideline for Go packages
- Standard Package Layout
- Internal packages in Go
- The init function · Effective Go
About pictures and reprints
Creative Commons Attribution 4.0 International License agreement
Wechat official account
About comments and comments
How to write elegant Golang code
How to Write elegant Golang code · Faith-oriented Programming
Follow: Draveness dead simple