This article has been authorized to be exclusively used by the public account of the Nuggets Developer community, including but not limited to editing and marking the original rights. This article has been authorized to be translated on the Nuggets developer platform with the consent of the original author. Tit Petric: scene-si.org/2018/07/24/…
I have been writing Go microservices for many years, and after writing two books on the subject of API Foundations in Go and 12 Factor Applications with Docker and Go, I have some ideas on how to write good Go code
But first, I want to explain something to those reading this article. Good code is subjective. You might have a completely different idea of what good code is, and we might only agree on some of it. On the other hand, neither of us may be wrong, but just because we approach engineering problems differently from both perspectives doesn’t mean that disagreement isn’t good code.
package
Packages are important, you might object – but if you’re writing microservices with Go, you can put all your code in one package. Of course, there are some counter-arguments:
- Put defined types in separate packages
- Maintain transport-independent service layers
- Maintain a repository layer outside of the service layer
We can calculate that the minimum number of microservice packs is 1. If you have a large microservice that has a Websocket and HTTP gateway, you may end up needing 5 packages (type, data store, service, WebSocket and HTTP package).
Simple microservices don’t really care about pulling business logic from the repository layer or from the transport layer (Websocket, HTTP). You can write simple code that converts data and responds, and it works. However, adding more packages can solve some of the problems. For example, if you are familiar with the SOLID principle, S stands for single responsibility. If we break it down into packages, these packages can be single responsibility.
types
– Declare some structures, maybe aliases for some structures, etcrepository
– Data storage layer, used to handle storage and read structuresservice
– The service layer, which wraps the concrete business logic implementation of the storage layerhttp
.websocket
,… – Transport layer, used to invoke the service layer
Of course, depending on what you use, you can further subdivide, for example, you can use types/ Request and types/ Response to better separate some structures. Instead of MessageRequest and MessageResponse, you can have Request. Message and Response. Message. It would have made more sense to split it up like this.
However, to emphasize the initial point – it doesn’t matter if you only use some of these declarative packages. A large project like Docker only uses the Types package under the Server package, which is what it really needs. Other packages it uses, like the Errors package, may be third-party packages.
It is also important to note that it is easy to share the structures and functions being processed within a package. If you have interdependent structures, splitting them into two or more different packages can cause diamond dependency problems. The solution is also obvious – put your code together, or put it all in one package.
Which one to choose? Either way. If I had to follow the rules, breaking it up into more packages would have made adding new code a hassle. Because you might have to modify these packages to add individual API calls. If you don’t know exactly how to lay it out, there can be some cognitive overhead in jumping between packages. In many cases, it is easier to read the code if the project has only one or two packages.
You certainly don’t want too many small bags.
error
Errors, if descriptive, may be the only tool a developer has to check for production problems. That’s why it’s important to handle errors gracefully, or to pass them all the way to a layer of the due program that receives and logs errors if they can’t be handled. Here are some features missing from the library error type:
- The error message does not contain a stack trace
- Can’t stack errors
- Errors is pre-instantiated
However, using third-party error packages (my favorite is PKG /Errors) can help solve these problems. There are other third-party error packages, but this one was written by Dave Cheney, the Go language guru, and it’s sort of a standard in the way errors are handled. Don’t Just Check Errors, Handle Them Gracefully is recommended.
Wrong stack trace
The PKG /errors package adds context (stack trace) to the newly created error when it calls errors.new.
users_test.go:34: testing error Hello world
github.com/crusttech/crust/rbac_test.TestUsers
/go/src/github.com/crusttech/crust/rbac/users_test.go:34
testing.tRunner
/usr/local/go/src/testing/testing.go:777
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:2361
Copy the code
Given that the full error message is “Hello world”, using fmt.Printf with a %+v argument or something like that to print a small amount of context is a great thing to do for finding errors. You can know exactly where the error (keyword) was created. Of course, when it comes to the standard library, the errors package and the native error type – do not provide a stack trace. However, one can easily be added using PKG /errors. Such as:
resp, err := u.Client.Post(fmt.Sprintf(resourcesCreate, resourceID), body)
iferr ! =nil {
return errors.Wrap(err, "request failed")}Copy the code
In the example above, the PKG /errors package adds context to err, and the added error message (” Request failed”) and stack trace are thrown out. Add a stack trace by calling errors.wrap, so you can accurately trace errors in this line.
Accumulation error
Your file system, database, or other errors may throw relatively poorly described errors. For example, Mysql might throw this mandatory error:
ERROR 1146 (42S02): Table 'test.no_such_table' doesn't exist
Copy the code
It’s not easy to handle. However, you can use errors.Wrap(err, “database aseError”) to stack new errors on it. This way, you can better handle “databaseError” and so on. The PKG/Errors package will retain the actual error information after the CAuser interface.
type causer interface {
Cause() error
}
Copy the code
This way, errors pile up and no context is lost. By the way, a mysql error is a type error that contains more information than just the error string. This means that it is likely to be handled better:
if driverErr, ok := err.(*mysql.MySQLError); ok {
if driverErr.Number == mysqlerr.ER_ACCESS_DENIED_ERROR {
// Handle the permission-denied error}}Copy the code
This example comes from this StackOverflow Thread.
Error preinstantiation
What is an error? Very simply, the error needs to implement the following interface:
type error interface {
Error() string
}
Copy the code
In the NET/HTTP example, the package exposes several error types as variables, as documented. Adding a stack trace here is not possible (Go does not allow executable code declarations on global var, only type declarations). Second, if the library adds a stack trace to the error – it does not point to where the error was returned, but to where the variable (global) was declared.
This means that you still need to force a call to something like return Errors.withstack (ErrNotSupported) in later code. This isn’t too painful, but unfortunately, you can’t just import PKG/Errors and have all existing errors with a stack trace. If you haven’t already instantiated your errors with errors.new, it requires some manual calls.
The log
Next comes logging, or more appropriately, structured logging. There are many packages available, similar to Sirupsen/Logrus or my favorite, APEX/LOG. These packages also support sending logs to remote machines or services that can be monitored using tools.
One option I don’t often see when it comes to standard logging packages is to create a custom Logger and pass flags like log.lshorfile or log.lUTC to it to get a bit of context again, which can make your life a lot easier – especially when dealing with servers in different time zones.
const (
Ldate = 1 << iota // the date in the local time zone: 2009/01/23
Ltime // the time in the local time zone: 01:23:23
Lmicroseconds // Microsecond Resolution: 01:23:23.123123. Assumes Ltime.
Llongfile // full file name and line number: /a/b/c/d.go:23
Lshortfile // final file name element and line number: d.go:23. overrides Llongfile
LUTC // if Ldate or Ltime is set, use UTC rather than the local time zone
LstdFlags = Ldate | Ltime // initial values for the standard logger
)
Copy the code
Even if you don’t create a custom logger, you can use SetFlags to change the default logger. Playground (link) :
package main
import (
"log"
)
func main(a) {
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Println("Hello, playground")}Copy the code
The results are as follows:
2009/11/10 23:00:00 main.go:9: Hello, playground
Copy the code
Don’t you want to know where you printed the log? This makes it easier to keep track of your code.
interface
If you are writing an interface and naming parameters in the interface, consider the following code snippet:
type Mover interface {
Move(context.Context, string.string) error
}
Copy the code
Do you know what the parameter here means? Just use named parameters in the interface to make it clear.
type Mover interface {
Move(context.Context, source string, destination string)}Copy the code
I also often see interfaces that use a specific type as a return value. An underutilized practice is to somehow declare the interface based on some known structure or interface parameters and then populate the result in the sink. This is probably one of the most powerful interfaces in Go.
type Filler interface {
Fill(r *http.Request) error
}
func (s *YourStruct) Fill(r *http.Request) error {
// here you write your code...
}
Copy the code
More likely, one or more constructs can implement the interface. As follows:
type RequestParser interface {
Parse(r *http.Request) (*types.ServiceRequest, error)
}
Copy the code
This interface returns a concrete type (instead of an interface). Often, such code will clutter up the interfaces in your code base, with only one implementation per interface and will become unavailable outside of your application package structure.
tips
If you want to make sure that your structure complies with and fully implements an interface (or interfaces) at compile time, you can do this:
var _ io.Reader = &YourStruct{}
var _ fmt.Stringer = &YourStruct{}
Copy the code
If you are missing some of the functions required by these interfaces, the compiler will report an error. The _ character represents a discarded variable, so there are no side effects, and the compiler completely optimizes the code to ignore the discarded lines.
Empty interface
This is probably a more controversial point than the one above – but I find using interface{} to be very effective sometimes. In the case of an HTTP API response, the final step is usually JSON encoding, which takes an interface parameter:
func (enc *Encoder) Encode(v interface{}) error
Copy the code
Therefore, it is entirely possible to avoid setting the API response to a specific type. I do not recommend this for all cases, but in some cases it is possible to ignore the specific type of the response completely in the API, or at least specify the meaning of the specific type declaration. One example that comes to mind is the use of anonymous constructs.
body := struct {
Username string `json:"username"`
Roles []string `json:"roles,omitempty"`
}{username, roles}
Copy the code
First, there is no way to return this structure from a function without using interface{}. Obviously, a JSON encoder can accept any type of content, so pressing the pass empty interface (to me) makes perfect sense. While the trend is to declare specific types, sometimes you may not need an intermediate layer. Empty interfaces are also suitable for functions that contain some logic and may return various forms of anonymous structures.
Correction: Anonymous constructs are not impossible to return, just cumbersome to do: playground
- Thank you @ikearens at Discord Gophers #golang Channel
The second use case is database-driven API design, which I’ve written about before, and I want to point out that it is very possible to implement a completely database-driven API. This also means that adding and modifying fields is done only in the database, without adding an additional layer of indirection in the form of an ORM. Obviously, you still need to declare the type to insert data into the database, but reading data from the database can omit the declaration.
// getThread fetches comments by data, order by ID
func (api *API) getThread(params *CommentListThread) (comments []interface{}, err error) {
// calculate pagination parameters
start := params.PageNumber * params.PageSize
length := params.PageSize
query := fmt.Sprintf("select * from comments where news_id=? and self_id=? and visible=1 and deleted=0 order by id %s limit %d, %d", params.Order, start, length)
err = api.db.Select(&comments, query, params.NewsID, params.SelfID)
return
}
Copy the code
Similarly, your application may act as a reverse proxy or use only schema-less database storage. In these cases, the purpose is simply to pass data.
One big caveat (and this is where you need to enter the structure) is that changing the interface value in Go is not an easy task. You must cast them to various content, such as a map, slice, or structure, so that the returned data can be accessed again. If you can’t keep the structure unchanged and just pass it from DB(or some other back-end service) to a JSON encoder (which involves assertions to specific types), then obviously this pattern isn’t for you. There should be no such empty interface code in this case. That is, empty interfaces are what you need when you don’t want to know anything about the payload.
Code generation
Use code generation whenever possible. If you want to generate mocks for testing, if you want to generate proc/grPC code, or any type of code generation you might have, you can just generate the code and submit it. In the event of a conflict, it can be discarded at any time and then rebuilt.
The only possible exception is to submit content similar to a public_html folder that contains content you will package using Rakyll/Statik. What if someone tried to tell me that the code generated by Gomock pollutes GIT history with megabytes of data every time it commits? It won’t.
conclusion
Another notable book on best and worst practices for Go should be Idiomatic Go. If you’re not familiar with it, read it – it’s a good match for this article.
I’d like to quote Jeff Atwood’s post – The Best Code is No Code At All with a memorable closing sentence:
If you really like to write code, you will very much like to write as little code as possible.
But do write those unit tests. The end.