Zen go



Evaluating my work, I recently thought a lot about how to write good code. Given that no one is interested in how to write bad code, the question arises: how do you know if you wrote good code on Go ? If there is some kind of scale between good and bad, then how to understand which parts of the scale belong to the good? What are its properties, attributes, distinguishing features, patterns and idioms?

Idiomatic go


These considerations led me to the idiomatic Go. If we call something “idiomatic”, then this something corresponds to a certain style of some time. If something is not idiomatic, then it does not correspond to the dominant style. That is not fashionable.

More importantly, when we say that someone’s code is not idiomatic, this doesn’t explain the reason. Why not idiomatic? The answer is given by the dictionary.

Idiom (n.): A speech revolution, used as a whole, not subject to further decomposition and usually not allowing permutations within itself.

Idioms are hallmarks of common meanings. Books will not teach you the idiomatic Go; it is only known when you become part of a community.

I am concerned about the idiomatic Go mantra because it is often restrictive. She says: "you cannot sit with us." Isn't that what we mean when we criticize someone else's work as “not idiomatic”? They did it wrong. This does not look right. This is not in keeping with the style of the times.

I believe that the idiomatic Go is not suitable for teaching how to write good code, because, in essence, it means telling people that they did something wrong. It is better to give such advice that will not push a person away at the moment when he most wants to receive this advice.

Sayings


Let's distract from idiomatic problems. What other cultural artifacts are inherent in Go programmers? Turn to the beautiful Go Proverbs page . Are these sayings a suitable learning tool? Do they tell beginners how to write good Go code?

I don’t think so. I do not want to belittle the work of the author. The sayings he composed are merely observations, not definitions of meanings. The dictionary comes to the rescue again:

Proverb (n.): A short statement that has literal or figurative meaning.

Go Proverbs' mission is to show the deep essence of the language architecture. But will it be useful to advice like "The empty interface does not say anything " to a beginner who came from a language without structural typing?

In a growing community, it is important to recognize that the number of Go students is far greater than the number of those who are fluent in this language. That is, sayings are probably not the best way to learn in such a situation.

Design Values


Dan Liu found an old presentation by Mark Lukowski on the design culture in the Windows NT-Windows 2000 Windows development team. I mentioned this because Lukowski describes culture as a common way of evaluating architectures and making compromises.


The main idea is to make value-based decisions within an unknown architecture . The NT team had these values: portability, reliability, security and extensibility. Simply put, design values ​​are a way to solve problems.

Go Values


What are Go's explicit values? What are the key concepts or philosophies that determine how Go programmers interpret the world? How are they proclaimed? How are they taught? How are they followed? How do they change over time?

How do you convert a Go programmer get the values ​​of Go design? Or how do you, an experienced Go-pro, proclaim your values ​​to future generations? And so that you understand, this process of knowledge transfer is not optional? Without the influx of new participants and new ideas, our community becomes myopic and withers.

Values ​​of other languages


To prepare the way for what I want to say, we can pay attention to other languages, to their design values.

For example, in C ++ and Rust it is believed that a programmer should not pay for a feature that he does not use . If the program does not use some resource-intensive feature of the language, then the program cannot be forced to bear the cost of maintaining this feature. This value is projected from the language into the standard library and is used as a criterion for evaluating the architecture of all programs written in C ++.

Main value in Java, Ruby and Smalltalk - everything is an object. This principle underlies program design in terms of message transfer, information hiding, and polymorphism. Architectures that conform to a procedural or functional paradigm are considered erroneous in these languages. Or, as a Go programmer would say, not idiomatic.

Let's get back to our community. What design values ​​do Go programmers profess? Discussions on this topic are often fragmented, so it is not easy to formulate a set of meanings. It is imperative to reach agreement, but the difficulty of reaching it grows exponentially with the growing number of participants in the discussion. But what if someone did this difficult job for us?

Zen Python Go


A few decades ago, Tim Peters sat down and wrote PEP-20 - The Zen of Python . He attempted to document the design values ​​that Guido Van Rossum adhered to as the Generous Lifetime Dictator of Python.

Let's look at The Zen of Python and see if we can learn anything about Go designer's design values.

A good package starts with a good name


Let's start with the sharp one:

Namespaces are a great idea, let's make them bigger!

The Zen of Python, record 19.

Unambiguously enough: Python programmers should use namespaces. Lots of spaces.

In Go terminology, a namespace is a package. There is no doubt that bundling favors design and reuse. But there may be confusion about how to do this, especially if you have many years of programming experience in another language.

In Go, every package must be designed for something. And the name is the best way to understand this destination. Reformulating Peteres’s thoughts, every package in Go should be designed for one thing.

The idea is not new, I have already talked about this . But why should this approach be used, and not another, in which packages are used for the needs of a detailed classification? It's all about the changes.

— , .


Change is the name of the game we are participating in. We, as programmers, manage change. If we do it well, we call it architecture. And if it’s bad, then we call it technical debt or legacy code.

If you write a program that works great once with one fixed set of input data, then nobody will be interested in whether it has good code, because only the result of its work is important for business.

But this does not happen . There are bugs in programs, requirements and input data change, and very few programs are written with a single execution expectation. That is, your program will change over time. Perhaps this task will be given to you, but most likely someone else will do it. Someone needs to accompany this code.

How do we make it easier to change programs? Add interfaces everywhere? Do everything suitable for creating stubs? Deploy dependencies tightly? Perhaps, for some types of programs, these techniques are suitable, but not for many. However, for most programs, creating a flexible architecture is more than design.

And if instead of expanding the components we will replace them? If the component does not do what is specified in the instructions, then it is time to change it.

A good package starts with choosing a good name. Consider it a short presentation that describes the function of a package with just one word. And when the name no longer meets the requirement, find a replacement.

Simplicity matters


Simple is better than complex.

The Zen of Python, entry 3.

PEP-20 claims that the simple is better than the complex, and I completely agree. A few years ago I wrote:


Most programming languages ​​try to be simple at first, but later decide to be powerful.

According to my observations, at least at that time, I could not remember a language I knew that would not be thought of as simple. As justification and temptation, the authors of each new language declared simplicity. But I found that simplicity was not the core value of many languages ​​of the same age as Go (Ruby, Swift, Elm, Go, NodeJS, Python, Rust). Perhaps this will hit a sore spot, but maybe the reason is that none of these languages ​​is simple. Or their authors did not consider them simple. Simplicity was not included in the list of core values.

You can consider me old-fashioned, but when did this simplicity go out of fashion? Why is the commercial software industry constantly and joyfully forgetting this fundamental truth?

There are two ways to create a software architecture: to make it so simple that the lack of flaws is obvious, and to make it so complex that it does not have obvious flaws. The first method is much more difficult.

Charles Hoar, The Emperor's Old Clothes, Turing Award Lecture, 1980

Simple does not mean easy, we know that. Often it takes more effort to ensure ease of use, rather than ease of creation.

Simplicity is the key to reliability.

Edsger Dijkstra, EWD498, June 18, 1975

Why strive for simplicity? Why is it important for Go programs to be simple? Simple means raw, it means readable and easy to follow. Simple does not mean artless, it means reliable, intelligible and understandable.

The core of programming is complexity management.

Brian Kernigan, Software Tools (1976)

Whether Python follows its mantra of simplicity is a debatable question. At Go, however, simplicity is a core value. I think we will all agree that in Go simple code is preferable to smart code.

Avoid package-level states


Explicit is better than implicit.

The Zen of Python, entry 2

Here, Peters, in my opinion, rather dreams than adheres to the facts. In Python, much is not explicit: decorators, dunder methods, etc. Undoubtedly, these are powerful tools, and they exist for a reason. On the implementation of each feature, especially complex, someone worked. But the active use of such features makes it difficult to evaluate the cost of the operation when reading the code.

Fortunately, we Go programmers can optionally make the code explicit. Perhaps, for you, manifestation may be synonymous with bureaucracy and verbosity, but this is a superficial interpretation. It will be a mistake to focus only on the syntax, to take care of the length of the lines and the application of DRY principles to expressions. It seems to me more important to provide explicitness in terms of connectedness and states.

Connectivity is a measure of the dependence of one on the other. If one is closely related to the other, then both move together. An action affecting one is directly reflected in the other. Imagine a train in which all the cars are connected — or rather, connected — together. Where the steam train goes, there are the cars.

Connectivity can also be described by the term cohesion - cohesion. This is a measure of how much one belongs to the other. In a soldered team, all the participants are so suited to each other, as if they were specially created that way.

Why is coherence important? As in the case of the train, when you need to change a piece of code, you have to change the rest of the closely related code. For example, someone has released a new version of their API, and now your code does not compile.

An API is an unavoidable source of binding. But it can be presented in more insidious forms. Everyone knows that if the signature of the API has changed, then the data transferred to and from the API will also change. It's all about the function signature: I take the values ​​of one type and return the values ​​of other types. And if the API begins to transfer data in a different way? What if the result of each API call depends on the previous call, even if you did not change your settings?

This is called state, and state management is a problem in computer science.

package counter

var count int

func Increment(n int) int {
        count += n
        return count
}

Here we have a simple package counter. To change the counter, you can call Increment, you can even get the value back if you increment with a zero value.

Let's say you need to test this code. How to reset the counter after each test? And if you want to run tests in parallel, how can this be done? And suppose you want to use several counters in the program, will you succeed?

Of course not. Obviously, the solution is to encapsulate the variable variablein the type.

package counter

type Counter struct {
        count int
}

func (c *Counter) Increment(n int) int {
        c.count += n
        return c.count
}

Now imagine that the described problem is not limited to counters; it also affects the main business logic of your applications. Can you test it in isolation? Can you test in parallel? Can you use multiple instances at the same time? If the answer is no for all questions, then the reason is the state at the packet level.

Avoid these conditions. Reduce connectivity and the number of nightmare remote actions by providing types with the dependencies they need as fields, rather than using package variables.

Make plans for failure, not success


Never pass bugs silently.

The Zen of Python, entry 10

This is said about languages ​​that encourage samurai-style exception handling: come back with a victory or don't come back at all. In languages ​​based on exceptions, functions return only valid results. If the function cannot do this, then the control flow goes in a completely different way.

Obviously, unchecked exceptions are an unsafe programming model. How can you write reliable code in the presence of errors if you do not know which expressions can throw an exception? Java tries to reduce risks with the concept of checked exceptions. And as far as I know, in other popular languages ​​there are no analogues of this solution. There are exceptions in many languages, and everywhere except Java, they are not checked.

Obviously, Go took a different path. Go programmers believe that reliable programs are made up of parts that handle failures before processing successful paths. Given that the language was created for server development, the creation of multi-threaded programs, as well as programs that process data entering over the network, programmers should focus on working with unexpected and damaged data, timeouts, and connection failures. Of course, if they want to make reliable products.

I believe that errors should be handled explicitly, this should be the main value of the language.

Peter Burgon, GoTime # 91

I join the words of Peter, they served as an impetus to the writing of this article. I believe that Go owes its success to explicit error handling. Programmers primarily think about possible crashes. First, we solve problems like “what if”. The result is programs in which failures are handled at the stage of writing code, and not as they happen during operation.

The verbosity of this code

if err != nil {
    return err
}

Outweighs the importance of deliberately handling each failed state at the time it occurs. The key to this is the value of explicitly handling each error.

Better to return early than to invest deeply


Sibling is better than nesting

The Zen of Python, entry 5

This wise advice comes from a language in which indentation is the main form of control flow. How do we interpret this tip in Go terminology? gofmt manages the entire amount of empty space in Go programs, so we have nothing to do here.

I wrote above about package names. Perhaps it is advisable to avoid a complex hierarchy of packages. In my experience, the more a programmer tries to separate and classify a code base on Go, the higher the risk of cyclical import of packages.

I believe that the best use of the fifth entry from The Zen of Python is to create a control flow inside a function. In other words, avoid a control flow that requires multi-level indentation.

Direct visibility is a straight line along which the view is not obscured by anything.

May Ryer, Code: Align the happy path to the left edge

May Ryer describes this idea as programming in direct line of sight:

  • Use control statements to return early if the precondition is not met.
  • Placing the statement of successful return at the end of the function, and not inside the conditional block.
  • Reduce the overall nesting level by extracting functions and methods.

Try to ensure that important functions never move out of line of sight to the right edge of the screen. This principle has a side effect: you will avoid meaningless disputes with the team about the length of the lines.

Each time you indent, you add one more precondition to the heads of programmers, occupying one of their 7 ± 2 short-term memory slots. Instead of deepening the nesting, try to keep the successful path of the function as close to the left side of the screen as possible.

If you think something is running slowly, then prove it with a benchmark


Give up the temptation to guess in the face of ambiguity.

The Zen of Python 12

Programming is based on mathematics and logic. These two concepts rarely use the element of luck. But we, as programmers, make numerous assumptions every day. What does this variable do? What does this option do? What happens if I pass nil here? What happens if I call the register twice? In modern programming, you have to assume a lot, especially when using other people's libraries.

The API should be easy to use and hard to misuse.

Josh Bloch

One of the best ways I've known to help a programmer avoid guessing when creating an API is to focus on standard usage methods . The caller should be able to perform normal operations as easily as possible. However, before I wrote a lot and talked about designing the API, so here is my interpretation of record 12: do not guess about the topic of performance .

Despite your attitude to Knut’s advice, one of the reasons for Go’s success is the effectiveness of its execution. Effective programs can be written in this language, and thanks to this, people willchoose go. There are many misconceptions related to performance. Therefore, when you are looking for ways to improve code performance, or follow dogmatic tips such as “shelving slows down,” “CGO is expensive,” or “always use atomic operations instead of mutexes,” don’t guess.

Do not complicate your code due to outdated dogmas. And if you think that something is working slowly, first make sure of it with the help of a benchmark. Go has great free benchmarking and profiling tools. Use them to find bottlenecks in the performance of your code.

Before starting gorutin, find out when it will stop


I think I have listed the valuable items from PEP-20 and perhaps expanded their interpretation beyond good taste. This is good, because although this is a useful rhetorical device, we are still talking about two different languages.

Write g, o, a space, and then a function call. Three button presses, it cannot be shorter. Three button clicks, and you launched the subprocess.

Rob Pike, Simplicity is Complicated , dotGo 2015

The next two tips I devote to the goroutines. Gorutins are a characteristic feature of the language, our response to high-level competitiveness. They are very easy to use: put a word goin front of the operator and you run the function asynchronously. No execution threads, no pool executors, no IDs, no completion status tracking.

Gorutins are cheap. Due to the ability of the runtime environment to multiplex goroutines in a small number of execution threads (which you do not need to manage), you can easily create hundreds of thousands or millions of goroutines. This allows you to create architectures that would be impractical when using other competitive models, in the form of execution threads or event callbacks.

But no matter how cheap the goroutines were, they are not free. Their stack takes at least a few kilobytes. And when you have millions of goroutines, it becomes noticeable. I do not mean to say that you do not need to use millions of goroutines, if architecture pushes you to this. But if you use it, then it is extremely important to monitor them, since in such quantities goroutines can consume a lot of resources.

Goroutines are the main source of ownership in Go. To be useful, goroutine must do something. That is, almost always it contains a link to a resource, that is, ownership information: lock, network connection, data buffer sending the end of the channel. While goroutine lives, the lock is held, the connection remains open, the buffer is saved, and channel recipients will wait for new data.

The simplest way to free resources is to link them to the goroutine life cycle. When it completes, resources are freed. And since it’s very easy to run goroutine, before you write “go and space” make sure that you have answers to these questions:

  • Under what condition does goroutine stop? Go cannot tell goroutine to end. For a specific reason, there is no function to stop or interrupt. We cannot order the goroutines to stop, but we can politely ask. This is almost always related to the operation of the channel. When it is closed, the range is looped to exit the channel. When closing the channel, you can select it. The signal from one goroutine to another is best expressed as a closed channel.
  • ? , , : ?
  • , ? , - . , . . , .


Probably in any of your serious Go programs, concurrency is used. This often leads to the problem of a worker pattern — one goroutine per connection.

A prime example is net / http. It is quite simple to stop the server that owns the listening socket, but what about the goroutines that are generated by this socket? net / http provides a context object inside the request object that can be used to tell the listening code that the request needs to be canceled, and therefore interrupt the goroutine. But it is not clear how to find out when all this needs to be done. It is one thing to call context.Cancel, another to know that the cancellation is completed.

I often find fault with net / http, but not because it is bad. On the contrary, it is the most successful, oldest and most popular API in the Go codebase. Therefore, its architecture, evolution, and flaws are carefully analyzed. Consider this flattery, not criticism.

So, I want to bring net / http as a counterexample of good practice. Since each connection is processed by the goroutin created inside the type net/http.Server, the program outside the net / http package cannot control the goroutins that are created by the receiving socket.

This area of ​​architecture is still developing. You can recall run.Groupthe go-kit, or ErrGroup, of the Go development team, which provides a framework for executing, canceling, and waiting for asynchronously executed functions.

For everyone who writes code that can be executed asynchronously, the main principle of creating architectures is that the responsibility for running goroutines should be shifted to the caller. Let him choose how he wants to run, track and wait for your functions to complete.

Write tests to block the behavior of your package API


You may have hoped that in this article I will not mention testing. Sorry, some other time.

Your tests are an agreement on what your program does and what does not. Unit tests should block the behavior of their APIs at the package level. Tests describe in code form what the package promises to do. If there is a unit test for each input conversion, then you, in the form of code , and not documentation, have defined an agreement on what the code will do.

Approving this agreement is as simple as writing a test. At any stage, you can state with a high degree of confidence that the behavior that people relied on before the changes you made will continue to function after the changes.

Tests block API behavior. Any changes that add, change or remove the public API should include changes in the tests.

Moderation is a virtue


Go is a simple language with only 25 keywords. In a way, this highlights the features built into the language. These are the features that allow the language to promote itself: simple competition, structural typing, etc.

I think all of us are confused by trying to use all the features of Go at once. How many of you were so inspired by the use of channels that you used them wherever you can? I found out that the resulting programs are difficult to test, they are fragile and too complex. And you?

I had the same experience with goroutines. Trying to divide the work into tiny fragments, I created the darkness of goroutin, which was difficult to control, and completely lost sight of the fact that most of them were always blocked due to the expectation of their predecessors to complete the work. The code was completely consistent, and I had to greatly increase the complexity in order to get a small advantage. How many of you have encountered this?

I had the same with embedding. At first I confused it with inheritance. Then he ran into the problem of a fragile base class, combining several complex types that already had several tasks into even more complex huge types.

This may be the least effective advice, but I consider it important to mention it. The advice is the same: keep moderation, and Go's capabilities are no exception. Whenever possible, do not use goroutines, channels, embedding structures, anonymous functions, an abundance of packages and interfaces. Use simpler solutions than smart ones.

Ease of maintenance matters


Finally, I’ll give you another entry from PEP-20:

Readability matters.

The Zen of Python, record 7

A lot has been said about the importance of code readability in all programming languages. Those who promote Go use words such as simplicity, readability, clarity, productivity. But all these are synonyms of one concept - convenience of maintenance.

The real goal is to create code that is easy to maintain. The code that outlives the author. A code that can exist not only as an investment of time, but as a basis for obtaining future value. This does not mean that readability is not important, just the convenience of maintenance is more important .

Go is not one of those languages ​​that are optimized for single-line programs. And not one of those languages ​​that are optimized for programs with a minimum number of lines. We do not optimize for the size of the source code on the disk, or for the speed of writing programs in the editor. We want to optimize our code so that it becomes more understandable for readers. Because it is they who will have to accompany him.

If you write a program for yourself, then perhaps it will be launched only once, or you are the only one who sees its code. In this case, do anything. But if more than one person works on the code, or if it will be used for a long time and the requirements, capabilities or runtime may change, then the program should be convenient to maintain. If the software cannot be maintained, then it cannot be rewritten. And this may be the last time your company invests in Go.

What you work hard on will be convenient to accompany after your departure? How can you facilitate the maintenance of your code for those who come after you today?

All Articles