Doing good doing bad: writing evil code with Go, Part 1

Original author: Jon Bodner
  • Transfer
  • Tutorial

Bad Tips for a Go Programmer


image

After decades of programming in Java, the last few years I mainly worked on Go. Working with Go is great, primarily because the code is very easy to follow. Java has simplified the C ++ programming model by removing multiple inheritance, manual memory management, and operator overloading. Go does the same, continuing to move toward a simple and straightforward programming style, completely removing inheritance and function overloading. Simple code is readable code, and readable code is supported code. And this is great for the company and my employees.

As in all cultures, software development has its own legends, stories that are retold by the water cooler. We all heard about developers who, instead of focusing on creating a quality product, focus on protecting their own work from outsiders. They don’t need supported code, because it means that other people will be able to understand and modify it. Is it possible on Go? Is it possible to make Go code so complicated? I will say right away - this is not an easy task. Let's look at the possible options.

You think: “ How much can you corrode code in a programming language? Is it possible to write such awful code on Go that its author becomes indispensable in the company?" Do not worry. When I was a student, I had a project in which I supported someone else's Lisp-e code written by a graduate student. In fact, he managed to write Fortran-e code using Lisp. The code looked something like this:

(defun add-mult-pi (in1 in2)
    (setq a in1)
    (setq b in2)
    (setq c (+ a b))
    (setq d (* 3.1415 c)
    d
)

There were dozens of files of such code. He was absolutely terrible and absolutely brilliant at the same time. I spent months trying to figure it out. Compared to this, writing bad code on Go is just a spit.

There are many different ways to make your code unsupported, but we will only look at a few. To do evil, you must first learn to do good. Therefore, we first look at how the "good" Go programmers write, and then we look at how to do the opposite.

Bad packaging


Packages are a handy topic to get started with. How can code organization impair readability?

In Go, the package name is used to refer to the exported entity (for example, ` fmt.Println` or ` http.RegisterFunc` ). Since we can see the name of the package, the “good” Go programmers make sure that this name describes what the exported entities are. We should not have util packages, because names like ` util.JSONMarshal` will not work for us - we need ` json.Marshal` .

The "good" Go developers also do not create a separate package for the DAO or model. For those who are not familiar with this term, the DAO is “ data access object”(data access object) ”- a code layer that interacts with your database. I used to work for a company where 6 Java services imported the same DAO library to access the same database, which they shared, because " ... well, you know, microservices are the same ... ".

If you have a separate package with all of your DAOs, then it is more likely that you will get a circular dependency between packages, which is forbidden in Go. And if you have several services that include this DAO package as a library, you may also encounter a situation where a change in one service requires updating all your services, otherwise something will break. This is called a distributed monolith and is incredibly difficult to update.

When you know how packaging should work and what makes it worse, “starting to serve evil” becomes simple. Poorly organize your code and give your packages bad names. Break your code into packages such as model , util and dao . If you really want to start creating chaos, try creating packages in honor of your cat or your favorite color. When people are faced with cyclic dependencies or distributed monoliths due to trying to use your code, you have to sigh, roll your eyes and tell them that they just do wrong ...

Inappropriate interfaces


Now that all of our packages are corrupted, we can move on to the interfaces. Interfaces in Go are not like interfaces in other languages. The fact that you do not explicitly declare that this type implements the interface at first seems insignificant, but in fact it completely reverses the concept of interfaces.

In most languages ​​with abstract types, an interface is defined before or at the same time as the implementation. You will have to do this at least for testing. If you do not create the interface in advance, you cannot insert it later without breaking all the code that uses this class. Because you have to rewrite it with a link to the interface instead of a specific type.

For this reason, Java code often has gigantic service interfaces with many methods. Classes that implement these interfaces then use the methods they need and ignore the rest. Writing tests is possible, but you add an additional level of abstraction, and when writing tests, you often resort to using tools to generate implementations of those methods that you do not need.

In Go, implicit interfaces determine which methods you need to use. Code owns an interface, not the other way around. Even if you use a type with many methods defined in it, you can specify an interface that includes only the methods you need. Another code using separate fields of the same type will define other interfaces that cover only the functionality that is needed. Typically, these interfaces have only a couple of methods.

This makes it easier to understand your code, because a method declaration not only determines what data it needs, but also accurately indicates what functionality it is going to use. This is one of the reasons why good Go developers follow the advice: " Accept interfaces, return structures ."

But just because this is good practice does not mean at all that you should do it ... The
best way to make your interfaces “evil” is to return to the principles of using interfaces from other languages, i.e. Define interfaces in advance as part of the code being called. Define huge interfaces with many methods that are used by all service clients. It becomes unclear what methods are really needed. This complicates the code, and complication, as you know, is the best friend of an “evil” programmer.

Pass heap pointers


Before explaining what this means, you need to philosophize a little. If you distract and think, each written program does the same thing. It receives data, processes it, and then sends the processed data to another location. This is so, regardless of whether you write a payroll system, accept HTTP requests and return web pages, or even check the joystick to track a button click - programs process the data.

If we look at the programs in this way, the most important thing to do is to make sure that it is easy for us to understand how the data is converted. And so it’s good practice to keep the data unchanged for as long as possible during the program. Because data that does not change is data that is easy to track.

In Go, we have reference types and value types. The difference between the two is whether the variable refers to a copy of the data or to the location of the data in memory. Pointers, slices, maps, channels, interfaces, and functions are reference types, and everything else is a value type. If you assign a value type variable to another variable, it creates a copy of the value; changing one variable does not change the value of another.

Assigning one variable of a reference type to another variable of a reference type means that they both share the same memory area, so if you change the data that the first points to, you change the data that the second points to. This is true for both local variables and function parameters.

func main() {
    //тип значений
    a := 1
    b := a
    b = 2
    fmt.Println(a, b) // prints 1 2
    //ссылочные типы
    c := &a
    *c = 3
    fmt.Println(a, b, *c) // prints 3 2 3
}

Kind Go developers want to make it easier to understand how data is collected. They try to use the type of values ​​as parameters of functions as often as possible. There is no way in Go to mark fields in structures or function parameters as final. If a function uses value parameters, changing the parameters will not change the variables in the calling function. All that the called function can do is return the value to the calling function. Thus, if you fill out a structure by calling a function with value parameters, you can not be afraid to transfer data to the structure, because you understand where each field in the structure came from.

type Foo struct {
    A int
    B string
}
func getA() int {
    return 20
}
func getB(i int) string {
    return fmt.Sprintf("%d",i*2)
}
func main() {
    f := Foo{}
    f.A = getA()
    f.B = getB(f.A)
    //Я точно знаю, что пришло в f
    fmt.Println(f)
}

Well, how do we become “evil”? Very simple - turning this model over.

Instead of calling functions that return the desired values, you pass a pointer to the structure in the function and allow them to make changes to the structure. Since each function has its own structure, the only way to find out which fields are changing is to look at the entire code. You may also have implicit dependencies between functions - the 1st function transfers the data needed by the 2nd function. But in the code itself, nothing indicates that you should first call the 1st function. If you build your data structures this way, you can be sure that no one will understand what your code is doing.

type Foo struct {
    A int
    B string
}
func setA(f *Foo) {
    f.A = 20
}
//Секретная зависимость для f.A!
func setB(f *Foo) {
    f.B = fmt.Sprintf("%d", f.A*2)
}
func main() {
    f := Foo{}
    setA(&f)
    setB(&f)
    //Кто знает, что setA и setB
    //делают и от чего зависят?
    fmt.Println(f)
}

Panic surfacing


Now we are starting to handle errors. You probably think that it’s bad to write programs that handle errors by about 75%, and I won’t say that you are wrong. Go code is often populated with head-to-toe error handling. And of course, it would be convenient to process them not so straightforward. Mistakes happen, and handling them is what sets professionals apart from beginners. Slurred error handling leads to unstable programs that are difficult to debug and difficult to maintain. Sometimes to be a “good” programmer means to “strain”.

func (dus DBUserService) Load(id int) (User, error) {
    rows, err := dus.DB.Query("SELECT name FROM USERS WHERE ID = ?", id)
    if err != nil {
        return User{}, err
    }
    if !rows.Next() {
        return User{}, fmt.Errorf("no user for id %d", id)
    }
    var name string
    err = rows.Scan(&name)
    if err != nil {
        return User{}, err
    }
    err = rows.Close()
    if err != nil {
        return User{}, err
    }
    return User{Id: id, Name: name}, nil
}

Many languages, such as C ++, Python, Ruby, and Java, use exceptions to handle errors. If something goes wrong, developers in these languages ​​throw or throw an exception, expecting some code to handle it. Of course, the program expects that the client is aware of a possible error being thrown at a given location so that it is possible to throw an exception. Because, except (with no pun intended) Java checked exceptions, there is nothing in the method signature in languages ​​or functions to indicate that an exception may occur. So how do developers know which exceptions to worry about? They have two options:

  • Firstly, they can read all the source code of all the libraries that their code calls, and all the libraries that call the called libraries, etc.
  • Secondly, they can trust the documentation. I may be biased, but personal experience does not allow me to fully trust the documentation.

So, how do we bring this evil to Go? Abusing panic ( panic ) and recovery ( recover ), of course! The panic is designed for situations such as “the drive fell off” or “the network card exploded.” But not for such - “someone passed string instead of int”.

Unfortunately, other, “less enlightened developers” will return errors from their code. Therefore, here is a small helper function of PanicIfErr. Use it to turn the mistakes of other developers into a panic.

func PanicIfErr(err error) {
    if err != nil {
        panic(err)
    }
}

You can use PanicIfErr to wrap other people's mistakes, compress code. No more ugly error handling! Any mistake is now a panic. It is so productive!

func (dus DBUserService) LoadEvil(id int) User {
    rows, err := dus.DB.Query(
                 "SELECT name FROM USERS WHERE ID = ?", id)
    PanicIfErr(err)
    if !rows.Next() {
        panic(fmt.Sprintf("no user for id %d", id))
    }
    var name string
    PanicIfErr(rows.Scan(&name))
    PanicIfErr(rows.Close())
    return User{Id: id, Name: name}
}

You can place the recovery somewhere closer to the beginning of the program, maybe in your own middleware . And then say that you not only process errors, but also make someone else’s code cleaner. To do evil by doing good is the best kind of evil.

func PanicMiddleware(h http.Handler) http.Handler {
    return http.HandlerFunc(
        func(rw http.ResponseWriter, req *http.Request){
            defer func() {
                if r := recover(); r != nil {
                   fmt.Println("Да, что-то произошло.")
                }
            }()
            h.ServeHTTP(rw, req)
        }
    )
}

Setting side effects


Next we will create a side effect. Remember, the “good” Go developer wants to understand how the data goes through the program. The best way to know what the data goes through is to set up explicit dependencies in the application. Even entities that correspond to the same interface can vary greatly in behavior. For example, a code that stores data in memory, and a code that accesses the database for the same work. However, there are ways to install dependencies in Go without explicit calls.

Like many other languages, Go has a way to magically execute code without invoking it directly. If you create a function called init with no parameters, it will automatically start when the package loads. And, to further confuse, if in one file there are several functions with the name init or several files in one package, they will all start.

package account
type Account struct{
    Id int
    UserId int
}
func init() {
    fmt.Println("Я исполняюсь магически!")
}
func init() {
    fmt.Println("Я тоже исполняюсь магически, и меня тоже зовут init()")
}

Init functions are often associated with empty imports. Go has a special way to declare imports, which looks like `import _“ github.com / lib / pq`. When you set an empty name identifier for an imported package, the init method runs in it, but it does not show any of the package identifiers. For some Go libraries — such as database drivers or image formats — you must load them by enabling empty package import, just to call the init function so that the package can register its code.

package main
import _ "github.com/lib/pq"
func main() {
    db, err := sql.Open(
        "postgres",
        "postgres://jon@localhost/evil?sslmode=disable")
}

And this is clearly an “evil” option. When you use initialization, code that works magically is completely outside the control of the developer. Best practices do not recommend using the initialization functions - these are non-obvious features, they confuse the code, and they are easy to hide in the library.

In other words, init functions are ideal for our evil purposes. Instead of explicitly configuring or registering entities in packages, you can use the initialization and empty import functions to configure the state of your application. In this example, we make the account available to the rest of the application through the registry, and the package itself is placed in the registry using the init function.

package account
import (
    "fmt"
    "github.com/evil-go/example/registry"
)
type StubAccountService struct {}
func (a StubAccountService) GetBalance(accountId int) int {
    return 1000000
}
func init() {
    registry.Register("account", StubAccountService{})
}

If you want to use an account, then put an empty import in your program. It does not have to be the main or related code - it just has to be “somewhere.” It `s Magic!

package main
import (
    _ "github.com/evil-go/example/account"
   "github.com/evil-go/example/registry"
)
type Balancer interface {
    GetBalance(int) int
}
func main() {
    a := registry.Get("account").(Balancer)
    money := a.GetBalance(12345)
}

If you use inits in your libraries to configure dependencies, you will immediately see that other developers are puzzling how these dependencies were installed and how to change them. And no one will be wiser than you.

Complicated Configuration


There is still a lot of everything that we can do with the configuration. If you are a “good” Go developer, you will want to isolate the configuration from the rest of the program. In the main () function, you get variables from the environment and convert them to the values ​​needed for components that are explicitly related to each other. Your components do not know anything about configuration files, or what their properties are called. For simple components, you set public properties, and for more complex ones, you can create a factory function that receives configuration information and returns a correctly configured component.

func main() {
    b, err := ioutil.ReadFile("account.json")
    if err != nil {
    fmt.Errorf("error reading config file: %v", err)
    os.Exit(1)
    }
    m := map[string]interface{}{}
    json.Unmarshal(b, &m)
    prefix := m["account.prefix"].(string)
    maker := account.NewMaker(prefix)
}
type Maker struct {
    prefix string
}
func (m Maker) NewAccount(name string) Account {
    return Account{Name: name, Id: m.prefix + "-12345"}
}
func NewMaker(prefix string) Maker {
    return Maker{prefix: prefix}
}

But the "evil" developers know that it is better to scatter the info about the configuration throughout the program. Instead of having one function in a package that defines the names and value types for your package, use a function that takes the config as it is and converts it on its own.

If this seems too "evil", use the init function to load the properties file from within your package and set the values ​​yourself. It may seem that you have made the lives of other developers easier, but you and I know ...

Using the init function, you can define new properties in the back of the code, and no one will ever find them until they get into production and everything falls off, because something will not get into one of the dozens of properties files needed to run. If you want even more “evil power”, you can suggest creating a wiki to keep track of all the properties in all libraries and to “forget” periodically add new ones. As a Property Keeper, you become the only person who can run the software.

func (m maker) NewAccount(name string) Account {
    return Account{Name: name, Id: m.prefix + "-12345"}
}
var Maker maker
func init() {
    b, _ := ioutil.ReadFile("account.json")
    m := map[string]interface{}{}
    json.Unmarshal(b, &m)
    Maker.prefix = m["account.prefix"].(string)
}

Functionality frameworks


Finally, we come to the topic of frameworks vs libraries. The difference is very subtle. It's not just about size; you can have large libraries and small frameworks. The framework calls your code while you call the library code yourself. Frameworks require you to write your code in a certain way, whether it be naming your methods according to specific rules, or that they correspond to specific interfaces, or force you to register your code in the framework. Frameworks have their own requirements for all of your code. That is, in general, frameworks command you.

Go encourages the use of libraries because libraries are linked. Although, of course, each library expects data to be transmitted in a specific format, you can write some connecting code to convert the output of one library into input for another.
It’s hard to get frameworks to work together seamlessly because every framework wants complete control over the code life cycle. Often the only way to get frameworks to work together is for the framework authors to come together and clearly organize mutual support. And the best way to use the “evil frameworks” to gain long-term power is to write your own framework, which is used only within the company.

Current and future evil


Having mastered these tricks, you will forever embark on the path of evil. In the second part, I will show how to deploy all this "evil", and how to correctly turn the "good" code into "evil".

Also popular now: