tl; dr magic is bad; global state is magic โ global variables in packages is bad; The init () function is not needed.
The most important and best property of Go is that it is, in fact, antimagic . Apart from a couple of exceptions, simply reading the Go code leaves no ambiguity in the definitions, dependencies, or behavior of runtime. This makes Go relatively easy to read, which in turn makes it easy to maintain, which is the most important feature in industrial programming.
But there are still a couple of places where magic can leak. One of the unfortunately common ways is to use the global state. Objects defined in the global package space can store a state or behavior that is hidden from an outside observer. Code that depends on these global objects can have unpleasant side effects that destroy the reader's ability to understand and build a mental model of the program.
Functions (including methods) are essentially the only mechanism that Go has to build abstractions. Let's look at the following function definition:
func NewObject(n int) (*Object, error)
By convention, we expect that the functions in the form NewXXX are type constructors. This wait is confirmed when we see that the function returns a pointer to an Object and an error. From this we can deduce that the constructor may not work, in which case it will return an error explaining the reason. We also see that the function takes as its input the only integer parameter that we believe controls some aspect or property of the object to be returned. Probably, there are some valid values โโof n, which, being violated, will lead to an error. But, since the function no longer accepts other parameters, we expect that the function no longer has any side effects, except (probably) for memory allocation.
Just by reading the function signature, we could draw all these conclusions and build a mental model of the function. This process, multiplied and repeated many times recursively starting from the first line of the main function, is how we read and understand programs.
Now, let's look at the function body:
func NewObject(n int) (*Object, error) { row := dbconn.QueryRow("SELECT ... FROM ... WHERE ...") var id string if err := row.Scan(&id); err != nil { logger.Log("during row scan: %v", err) id = "default" } resource, err := pool.Request(n) if err != nil { return nil, err } return &Object{ id: id, res: resource, }, nil }
The function uses the global object from the sql - database / sql.Conn package to make a query to some incomprehensible database; then the global for the package logger to write the string in an incomprehensible format in an incomprehensible where; and the global request pool object to request a resource. All these operations have side effects that are completely invisible when reading the function signature. A programmer reading it has no way to predict any of these actions, except for reading the function body and peering into the definitions of global variables.
Let's try this signature option:
func NewObject(db *sql.DB, pool *resource.Pool, n int, logger log.Logger) (*Object, error)
Simply by raising all these dependencies as parameters, we allowed the programmer to accurately model the dependencies and behavior of the function. Now he knows exactly what the function needs to do his job, and he can provide everything he needs.
If we were developing a public API for this package, we could go even further:
// RowQueryer models part of a database/sql.DB. type RowQueryer interface { QueryRow(string, ...interface{}) *sql.Row } // Requestor models the requesting side of a resource.Pool. type Requestor interface { Request(n int) (*resource.Value, error) } func NewObject(q RowQueryer, r Requestor, logger log.Logger) (*Object, error) { // ... }
By simulating each specific object as an interface containing only the methods we need, we allowed the calling code to easily switch between implementations. This reduces connectivity between packages, and allows us to easily mock specific dependencies in tests. Testing the original version of the code, with specific global variables, includes tedious and error-prone substitution of components.
If all our constructors and functions accepted dependencies explicitly , we would not need global variables at all. Instead, we could create all of our database connections, loggers, and resource pools in the main function, allowing future code readers to understand the component graph very clearly. And we can very clearly transfer all our dependencies, removing the magic of global variables, which is harmful for understanding. Also note that if we do not have global variables, we no longer need the init function, whose only function is to create or change the global state of the package. We can then look at all cases of using int functions with a fair suspicion: what exactly does this code do? If it is not in the main function, why is it?
And it is not just possible, but very simple, and, in fact, very refreshing, to write Go programs, in which there is practically no global state. From my experience, programming in this way is no slower and more boring than using global variables to reduce function definitions. Even on the contrary: when the function signature reliably and fully describes its behavior, we can argue, refactor, and maintain the code in large code bases much more efficiently. Go kit was written in this style from the very beginning, and only benefited from this.
-
And at this moment I can formulate the theory of modern Go. Based on the words of Dave Cheney, I propose the following rules:
Of course, there are exceptions. But following these rules, other practices appear naturally.
Source: https://habr.com/ru/post/330786/
All Articles