Bad Tips for a Go Programmer
In the first part of the publication, I explained how to become a "malicious" Go programmer. Evil comes in many forms, but in programming it lies in the deliberate difficulty of understanding and maintaining the code. Evil programs ignore the basic means of language in favor of techniques that provide short-term benefits in exchange for long-term problems. As a brief reminder, Go’s evil “practices” include:
')
- Poorly named and organized packages
- Incorrectly organized interfaces
- Passing pointers to variables in functions to populate their values
- Using panic instead of mistakes
- Using init functions and empty imports to configure dependencies
- Download configuration files using init functions
- Using frameworks instead of libraries
Big ball of evil
What happens if all our evil practices are brought together? We would have a framework that would use many configuration files, fill out the structure fields with pointers, define interfaces for describing published types, rely on “magic” code and panic whenever a problem occurs.
And I did it. If you go to
https://github.com/evil-go , you will see
Fall , a DI framework that allows you to implement any “evil” practices that you want. I soldered Fall with a tiny Outboy web framework that follows the same principles.
You may ask how villainous they are? Let's get a look. I suggest going for a simple Go program (written using best practices) that provides http endpoint. And then rewrite it using Fall and Outboy.
Best practics
Our program lies in a single package called greet, which uses all the basic functions to implement our endpoint. Since this is an example, we use a working in memory DAO, with three fields for the values ​​we will return. We will also have a method that, depending on the input, replaces the call to our database and returns the desired greeting.
package greet type Dao struct { DefaultMessage string BobMessage string JuliaMessage string } func (sdi Dao) GreetingForName(name string) (string, error) { switch name { case "Bob": return sdi.BobMessage, nil case "Julia": return sdi.JuliaMessage, nil default: return sdi.DefaultMessage, nil } }
Next is the business logic. To implement it, we define a structure for storing output data, a GreetingFinder interface for describing what business logic is looking for at the data search level, and a structure for storing business logic itself with a field for GreetingFinder. The actual logic is simple - it just calls GreetingFinder and handles any errors that might occur.
type Response struct { Message string } type GreetingFinder interface { GreetingForName(name string) (string, error) } type Service struct { GreetingFinder GreetingFinder } func (ssi Service) Greeting(name string)(Response, error) { msg, err := ssi.GreetingFinder.GreetingForName(name) if err != nil { return Response{}, err } return Response{Message: msg}, nil }
Then comes the web layer, and for this part we define the Greeter interface, which provides all the business logic we need, as well as the structure containing the http handler configured using Greeter. Then we create a method to implement the http.Handler interface, which splits the http request, calls greeter (welder), processes errors, and returns the results.
type Greeter interface { Greeting(name string) (Response, error) } type Controller struct { Greeter Greeter } func (mc Controller) ServeHTTP(rw http.ResponseWriter, req *http.Request) { result, err := mc.Greeter.Greeting( req.URL.Query().Get("name")) if err != nil { rw.WriteHeader(http.StatusInternalServerError) rw.Write([]byte(err.Error())) return } rw.Write([]byte(result.Message)) }
This is the end of the greet package. Next, we will see how a “good” Go developer would write main to use this package. In the main package, we define a structure called Config, which contains the properties we need to run. The main function then does 3 things.
- First, it calls the loadProperties function, which uses a simple library ( https://github.com/evil-go/good-sample/blob/master/config/config.go ) to load properties from the config file and places them in our copy of a config. If the configuration load failed, the main function reports an error and exits.
- Secondly, the main function binds components in the greet package, explicitly assigning values ​​from the config to them and setting up dependencies.
- Thirdly, it calls a small server library ( https://github.com/evil-go/good-sample/blob/master/server/server.go ) and passes the address, HTTP method and http.Handler to endpoint for request processing. A library call launches a web service. And this is our entire application.
package main type Config struct { DefaultMessage string BobMessage string JuliaMessage string Path string } func main() { c, err := loadProperties() if err != nil { fmt.Println(err) os.Exit(1) } dao := greet.Dao{ DefaultMessage: c.DefaultMessage, BobMessage: c.BobMessage, JuliaMessage: c.JuliaMessage, } svc := greet.Service{GreetingFinder: dao} controller := greet.Controller{Greeter: svc} err = server.Start(server.Endpoint{c.Path, http.MethodGet, controller}) if err != nil { fmt.Println(err) os.Exit(1) } }
The example is pretty short, but it shows how cool Go is written; some things are ambiguous, but in general it is clear what is happening. We glue small libraries that are specifically set up to work together. Nothing is hidden; anyone can take this code, understand how its parts are connected together, and if necessary redo them to new ones.
Black spot
Now we will consider the version of Fall and Outboy. The first thing we will do is break the greet package into several packages, each of which contains one application layer. Here is the DAO package. It imports Fall, our DI framework, and since we are “evil” and define relationships with interfaces the other way around, we will define an interface called GreetDao. Please note - we have removed all links to errors; if something is wrong, we just panic. At this point, we already have poor packaging, bad interfaces, and bad bugs. Great start!
We slightly renamed our structure from a good example. Fields now have struct tags; they are used to make Fall set the registered value in the field. We also have an init function for our package, with which we accumulate “evil power”. In the package init function, we call Fall twice:
- Once to register a config file that provides values ​​for structure tags.
- And another, to register a pointer to an instance of the structure. Fall will be able to fill in these fields for us and make the DAO available for use by other code.
package dao import ( "github.com/evil-go/fall" ) type GreetDao interface { GreetingForName(name string) string } type greetDaoImpl struct { DefaultMessage string `value:"message.default"` BobMessage string `value:"message.bob"` JuliaMessage string `value:"message.julia"` } func (gdi greetDaoImpl) GreetingForName(name string) string { switch name { case "Bob": return gdi.BobMessage case "Julia": return gdi.JuliaMessage default: return gdi.DefaultMessage } } func init() { fall.RegisterPropertiesFile("dao.properties") fall.Register(&greetDaoImpl{}) }
Let's see the service package. It imports the DAO package because it needs access to the interface defined there. The service package also imports the model package, which we have not yet considered - we will store our data types there. And we import Fall, because, like all "good" frameworks, it penetrates everywhere. We also define an interface for service to give access to the web layer. Again, without error handling.
The implementation of our service now has a structural tag with wire. The field marked wire automatically connects its dependency when the structure is registered in Fall. In our tiny example, it is clear what will be assigned to this field. But in a larger program, you will only know that somewhere this GreetDao interface is implemented, and it is registered in Fall. You cannot control dependency behavior.
Next is the method of our service, which has been slightly modified to get the GreetResponse structure from the model package, and which removes any error handling. Finally, we have an init function in the package that registers a service instance in Fall.
package service import ( "github.com/evil-go/fall" "github.com/evil-go/evil-sample/dao" "github.com/evil-go/evil-sample/model" ) type GreetService interface { Greeting(string) model.GreetResponse } type greetServiceImpl struct { Dao dao.GreetDao `wire:""` } func (ssi greetServiceImpl) Greeting(name string) model.GreetResponse { return model.GreetResponse{Message: ssi.Dao.GreetingForName(name)} } func init() { fall.Register(&greetServiceImpl{}) }
Now let's look at the model package. There is nothing to look at. It can be seen that the model is separated from the code that creates it, only to divide the code into layers.
package model type GreetResponse struct { Message string }
In the web package we have a web interface. Here we import both Fall and Outboy, as well as import the service package on which the web package depends. Because frameworks only work well together when they are integrated backstage, Fall has special code to make sure that it and Outboy work together. We are also changing the structure so that it becomes the controller for our web application. She has two fields:
- The first is connected through Fall to the implementation of the GreetService interface from the service package.
- The second is the path for our only web endpoint. It is assigned the value from the config file registered in the init function of this package.
Our http handler has been renamed GetHello and it is now free from error handling. We also have the Init method (with a capital letter), which should not be confused with the init function. Init is a magic method that is called for structures registered in Fall after filling in all the fields. In Init, we call Outboy to register our controller and its endpoint in the path that was set using Fall. Looking at the code, you will see the path and handler, but the HTTP method is not specified. In Outboy, the method name is used to determine which HTTP method the handler responds to. Since our method is called GetHello, it responds to GET requests. If you do not know these rules, you will not be able to understand what requests he answers. True, this is very villainous?
Finally, we call the init function to register the config file and controller in Fall.
package web import ( "github.com/evil-go/fall" "github.com/evil-go/outboy" "github.com/evil-go/evil-sample/service" "net/http" ) type GreetController struct { Service service.GreetService `wire:""` Path string `value:"controller.path.hello"` } func (mc GreetController) GetHello(rw http.ResponseWriter, req *http.Request) { result := mc.Service.Greeting(req.URL.Query().Get("name")) rw.Write([]byte(result.Message)) } func (mc GreetController) Init() { outboy.Register(mc, map[string]string{ "GetHello": mc.Path, }) } func init() { fall.RegisterPropertiesFile("web.properties") fall.Register(&GreetController{}) }
It remains only to show how we run the program. In the main package, we use empty imports to register Outboy and the web package. And the main function calls fall.Start () to launch the entire application.
package main import ( _ "github.com/evil-go/evil-sample/web" "github.com/evil-go/fall" _ "github.com/evil-go/outboy" ) func main() { fall.Start() }
Disruption of the integument
And here she is, a complete program written using all of our evil Go tools. A nightmare. She magically hides how parts of the program fit together, and makes it terribly difficult to understand her work.
And yet, you must admit that there is something attractive about writing code with Fall and Outboy. For a tiny program, you could even say that is an improvement. See how easy it is to configure! I can connect dependencies with virtually no code! I registered a handler for the method, just using its name! And without any error handling, everything looks so clean!
That is how evil works. At first glance, it is really attractive. But as your program changes and grows, all this magic only begins to interfere, complicating the understanding of what is happening. Only when you are completely obsessed with evil do you look back and realize that you are trapped.
For Java developers, this may seem familiar. These techniques can be found in many popular Java frameworks. As I mentioned earlier, I have been working with Java for over 20 years, starting from 1.0.2 in 1996. In many cases, Java developers were the first to encounter problems writing large-scale enterprise software in the Internet age. I remember the times when servlets, EJB, Spring, and Hibernate just appeared. The decisions that Java developers made at that time made sense. But over the years, these techniques show their age. Newer languages, such as Go, are designed to eliminate the pain points found when using older techniques. However, as Java developers begin to learn Go and write code with it, they should remember that trying to reproduce patterns from Java will produce bad results.
Go was designed for serious programming - for projects that span hundreds of developers and dozens of teams. But for Go to do this, you need to use it the way it works best. We can choose to be evil or good. If we choose evil, we can encourage young Go developers to change their style and techniques before they understand Go. Or we can choose good. Part of our work as Go developers is to educate young Gophers (Gophers), to help them understand the principles that underlie our best practices.
The only drawback to following the path of good is that you have to look for another way to express your inner evil.
Maybe try driving at a speed of 30 km / h on the federal highway?