📜 ⬆️ ⬇️

Practical Go: tips on writing supported programs in the real world

The article is devoted to the best practices of writing Go code. It is made in the style of the presentation, but without the usual slides. We will try to briefly and clearly walk through each item.

First you need to agree on what the best practices for a programming language mean. Here you can recall the words of Russ Cox, Go technical manager:

Software engineering is what happens with programming if you add the time factor to other programmers.

Thus, Russ distinguishes between the concepts of programming and software engineering . In the first case, you write a program for yourself; in the second, you create a product, over which other programmers will work over time. Engineers come and go. Teams grow or shrink. New features are added and bugs are fixed. That is the nature of software development.

Content



1. Fundamental principles


Perhaps among you I am one of the first Go users, but this is not my personal opinion. These basic principles underpin Go itself:
')
  1. Simplicity
  2. Readability
  3. Productivity

Note. Note that I did not mention "performance" or "parallelism". There are languages ​​faster Go, but definitely they can not be compared in simplicity. There are languages ​​that make concurrency a top priority, but they do not equal either in readability or in programming productivity.

Performance and concurrency are important attributes, but not as important as simplicity, readability and productivity.

Simplicity


“Simplicity is a prerequisite for readability” - Edsger Dijkstra

Why strive for simplicity? Why is it important that Go programs are simple?

Each of us came across an incomprehensible code, right? When you are afraid to make an edit, because it will break another part of the program that you do not quite understand and do not know how to fix it. This is the complexity.

“There are two ways to design software: the first is to make it so simple that there are no obvious flaws, and the second is to make it so complicated that there are no obvious flaws. The first is much more difficult. ” - C. E. R. Hoar

The complexity turns reliable software into unreliable. The complexity is what kills software projects. Therefore, simplicity is the ultimate goal of Go. Whatever programs we write, they should be simple.

1.2. Readability


“Readability is an integral part of maintainability” - Mark Reinhold, JVM conference, 2018

Why is it important that the code is readable? Why should we strive for readability?

“Programs should be written for people, and machines only execute them” - Hal Abelson and Gerald Sassman, “Structure and Interpretation of Computer Programs”

Not only Go programs, but in general all software is written by people for people. The fact that machines also process the code is secondary.

Once written, the code will be repeatedly read by people: hundreds or even thousands of times.

“The most important skill for a programmer is the ability to effectively convey ideas” - Gaston Horker

Readability is the key to understanding what the program does. If you can not understand the code, how to support it? If the software cannot be maintained, it will be rewritten; and this may be the last time your company uses Go.

If you are writing a program for yourself, do what works for you. But if this is part of a joint project or program will be used long enough to change the requirements, functions, or environment in which it works, then your goal is to make the program maintainable.

The first step to writing supported software is to make sure the code is clear.

1.3. Productivity


“Design is the art of organizing code so that it works today, but always supports change.” - Sandy Mets

As the last basic principle I want to name the developer productivity. This is a big topic, but it comes down to the ratio: how much time you spend on useful work, and how much - on waiting for a response from tools or hopeless wandering in an incomprehensible code base. Go programmers need to feel that they can handle a lot of work.

There is a joke that the Go language was developed while the C ++ program was compiled. Quick compilation is a key feature of Go and a key factor in attracting new developers. Although compilers are improving, in general, minute compilations in other languages ​​take place in a few seconds on Go. So Go developers feel as productive as programmers in dynamic languages, but without problems with the reliability of those languages.

If it is fundamental to talk about developer productivity, then Go programmers understand that reading the code is essentially more important than writing it. In this logic, Go even goes so far as using the tools to format all the code in a particular style. This eliminates the slightest difficulty in learning a specific dialect of a particular project and helps to identify errors, because they just look wrong compared to ordinary code.

Go programmers do not spend days debugging strange compilation errors, complex build scripts, or deploying code in a production environment. And most importantly, they do not waste time trying to understand what a colleague wrote.

When the Go developers talk about the scalability of a language, they mean exactly productivity.

2. Identifiers


The first topic we’ll discuss is identifiers , which is a synonym for names : the names of variables, functions, methods, types, packages, and so on.

“A bad name is a symptom of bad design” - Dave Cheney

Given the limited syntax of Go, object names have a huge impact on the readability of programs. Readability is a key factor in good code, so choosing good names is crucial.

2.1. Name identifiers for clarity, not brevity.


“It is important that the code is obvious. What can be done in one line, you must do in three " - Ukiya Smith

Go is not optimized for tricky one-liners or the minimum number of lines in the program. We do not optimize the size of the source code on the disk, nor the time required to set the program in the editor.

“A good name is like a good joke. If you need to explain it, it's not funny anymore. ” - Dave Cheney

The key to maximum clarity is the names we choose to identify programs. What are the qualities of a good name?


Let's take a closer look at each of these properties.

2.2. Id length


Sometimes Go style is criticized for short variable names. As Rob Pike said , “Go programmers want identifiers of the correct length.”

Andrew Gerrand suggests using long identifiers to indicate importance.

“The greater the distance between the announcement of the name and the use of the object, the longer the name must be” - Andrew Gerrand

Thus, you can make some recommendations:


Consider an example.

type Person struct { Name string Age int } // AverageAge returns the average age of people. func AverageAge(people []Person) int { if len(people) == 0 { return 0 } var count, sum int for _, p := range people { sum += p.Age count += 1 } return sum / count } 

The tenth line declares the variable of the range p , and it is called only once from the next line. That is, the variable lives on the page for a very short time. If the reader is interested in the role of p in the program, it is enough to read only two lines.

For comparison, people declared in the parameters of the function and live seven lines. The same applies to sum and count , so they justify their longer names. The reader needs to scan more code to find them: this justifies more distinctive names.

You can choose s for sum and c (or n ) for count , but this will reduce the importance of all variables in the program to the same level. You can replace people with p , but there will be a problem, how to call the iteration variable for ... range . A single person will look weird, because a short-lived iteration variable produces a longer name than several values ​​from which it is derived.

Council Separate the function flow with empty lines, as empty lines between paragraphs break the flow of text. In AverageAge we have three consecutive operations. First, check the division by zero, then the total age and the number of people, and the last is the calculation of the average age.

2.2.1. The main thing is the context


It is important to understand that most naming tips are context sensitive. I like to say that this is a principle, not a rule.

What is the difference between i and index ids? For example, it is impossible to say for sure that such code

 for index := 0; index < len(s); index++ { // } 

fundamentally more readable than

 for i := 0; i < len(s); i++ { // } 

I believe that the second option is not worse, because in this case the area i or index limited to the body of the for loop, and the additional verbosity adds little to the understanding of the program.

But which of these functions is more readable?

 func (s *SNMP) Fetch(oid []int, index int) (int, error) 

or

 func (s *SNMP) Fetch(o []int, i int) (int, error) 

In this example, oid is an abbreviation of SNMP Object ID, and an additional abbreviation to o causes the code to go from documented notation to shorter notation in code. Similarly, the reduction of index to i makes it difficult to understand the essence, since in SNMP messages, the sub value of each OID is called an index.

Council Do not combine long and short formal parameters in one ad.

2.3. Do not name variables by their types.


You will not call your pets "dog" and "cat", right? For the same reason, do not include the type name in the variable name. It should describe the content, not its type. Consider an example:

 var usersMap map[string]*User 

What good is this ad? We see that this is a map, and it has something to do with the type *User : this is probably good. But usersMap is really a map, and Go, as a statically typed language, will not accidentally use such a name where a scalar variable is required, therefore the Map suffix is ​​redundant.

Consider the situation when other variables are added:

 var ( companiesMap map[string]*Company productsMap map[string]*Products ) 

Now we have three variables of type map: usersMap , companiesMap and productsMap , and all strings are matched with different types. We know that these are maps, and we also know that the compiler will give an error if we try to use companiesMap where the code expects map[string]*User . In this situation, it is clear that the Map suffix does not improve the clarity of the code, it’s just extra characters.

I suggest avoiding any suffixes that resemble the type of a variable.

Council If the title users insufficiently clearly describes the essence, then usersMap too.

This tip also applies to function parameters. For example:

 type Config struct { // } func WriteConfig(w io.Writer, config *Config) 

The config name for the *Config parameter is redundant. We already know that this is *Config , right there next written.

In this case, consider conf or c if the lifetime of the variable is short enough.

If at some point in our area more than one *Config , then the names conf1 and conf2 less meaningful than the original and updated , since the latter are more difficult to confuse.

Note Don't let package names steal good variable names.

The name of the identifier being imported contains the name of the package. For example, the Context type in the context package will be called context.Context . This makes it impossible to use a variable or type context in your package.

 func WriteLog(context context.Context, message string) 

This will not compile. That is why the local declaration of context.Context types, for example, traditionally uses names like ctx .

 func WriteLog(ctx context.Context, message string) 

2.4. Use a single naming style.


Another property of a good name - it must be predictable. The reader should immediately understand him. If this is a common name, then the reader has the right to assume that it has not changed the meaning from the previous time.

For example, if the code goes around a database handle, each time the parameter is displayed, it must have the same name. Instead of all combinations of types d *sql.DB , dbase *sql.DB , DB *sql.DB and database *sql.DB it is better to use one thing:

 db *sql.DB 

It's easier to understand the code. If you see a db , you know that it is *sql.DB and it is declared locally or provided by the caller.

Similar advice regarding method recipients; use the same recipient name for each method of this type. So it will be easier for the reader to grasp the use of the recipient among various methods of this type.

Note The agreement on short names of recipients in Go contradicts the previously voiced recommendations. This is one of those cases where the choice made at an early stage becomes a standard style, like using CamelCase instead of snake_case .

Council The Go style indicates single letter names or abbreviations for recipients, derived from their type. It may turn out that the receiver's name sometimes conflicts with the name of the parameter in the method. In this case, it is recommended to make the parameter name a little longer and not to forget to use it consistently.

Finally, some single-letter variables are traditionally associated with cycles and counting. For example, i , j and k are usually inductive variables in for cycles, n usually associated with a counter or accumulator, v is a typical value reduction in the coding function, k usually used for a card key, and s often used as an abbreviation for string parameters .

As in the db example above, programmers expect i be an inductive variable. If they see it in code, then they expect to see a loop soon.

Council If you have so many nested loops that you have exhausted the supply of variables i , j and k , then you should split the function into smaller units.

2.5. Use a single declaration style.


Go has at least six different ways to declare a variable.


Sure, I have not remembered everything. Go developers, probably, consider this a mistake, but it is already too late to change something. With this choice, how to ensure a uniform style?

I want to offer a style of variable declaration, which I myself try to use wherever possible.


Since there are no automatic conversions from one type to another in Go, in the first and third examples, the type on the left side of the assignment operator must be identical to the type on the right side. The compiler can deduce the type of the declared variable from the type on the right, so that an example can be written more concisely:

 var players = 0 var things []Thing = nil var thing = new(Thing) json.Unmarshall(reader, thing) 

Here, the players explicitly initialized to 0 , which is redundant, because the initial value of the players is zero anyway. Therefore, it is better to make it clear that we want to use a zero value:

 var players int 

What about the second operator? We cannot determine the type and write

 var things = nil 

Because nil no type . Instead, we have a choice: or we use the zero value for the slice ...

 var things []Thing 

... or create a slice with zero elements?

 var things = make([]Thing, 0) 

In the second case, the value for the slice is not zero, and we let the reader understand this using a short form of the announcement:

 things := make([]Thing, 0) 

This tells the reader that we decided to explicitly initialize things .

So we come to the third declaration:

 var thing = new(Thing) 

Here, both the explicit initialization of the variable and the introduction of the "unique" keyword new , which some Go programmers do not like, are simultaneously. If we apply the recommended short syntax, we get

 thing := new(Thing) 

This makes it clear that thing explicitly initialized to the result of new(Thing) , but still leaves an atypical new . The problem could be solved with the help of a literal:

 thing := &Thing{} 

That is similar to new(Thing) , and such duplication grieves some Go programmers. However, this means that we explicitly initialize a thing with a pointer to Thing{} and a zero value of Thing .

But it is better to take into account the fact that the thing declared with a zero value, and use the operator’s address to transfer the address of the thing to json.Unmarshall :

 var thing Thing json.Unmarshall(reader, &thing) 

Note Of course, there are exceptions to any rule. For example, sometimes two variables are closely related, so it would be strange to write

 var min int max := 1000 

More readable declaration:

 min, max := 0, 1000 

Summarize:


Council Explicitly point out complex things.

 var length uint32 = 0x80 

Here, length can be used with a library, which requires a specific numeric type, and this option more explicitly indicates that the type of length is specifically chosen as uint32 than in the short declaration:

 length := uint32(0x80) 

In the first example, I intentionally break my rule by using the var declaration with explicit initialization. A departure from the standard makes the reader understand that something unusual is happening.

2.6. Work for a team


I have already said that the essence of software development is the creation of readable, supported code. Probably most of your career will be working on joint projects. My advice in this situation: follow the style adopted in the team.

Changing styles in the middle of a file is annoying. It is important uniformity, albeit to the detriment of personal preferences. My rule of thumb is: if the code comes up through gofmt , then usually the problem is not worth discussing.

Council If you want to rename the entire code base, do not mix it with other changes. If someone uses git bisect, he will not like to wade through thousands of renames to find another altered code.

3. Comments


Before we move on to more important points, I want to give a couple of minutes to the comments.

“A good code has a lot of comments, and a bad code requires a lot of comments” - Dave Thomas and Andrew Hunt, “Pragmatic Programmer”

Comments are very important for the readability of the program. Each comment should make one - and only one - of three things:

  1. Explain what the code does.
  2. Explain how he does it.
  3. Explain why .

The first form is ideal for comments on public symbols:

 // Open     . //           . 

The second is ideal for comments inside the method:

 //     var results []chan error for _, dep := range a.Deps { results = append(results, execute(seen, dep)) } 

The third form (“why”) is unique in that it does not displace and does not replace the first two. Such comments explain the external factors that led to the writing of the code in its current form. Often, without this context, it is difficult to understand why the code is written this way.

 return &v2.Cluster_CommonLbConfig{ //  HealthyPanicThreshold HealthyPanicThreshold: &envoy_type.Percent{ Value: 0, }, } 

In this example, it may not immediately be clear what happens when the HealthyPanicThreshold setting is zero percent. The comment is intended to clarify that a value of 0 disables the panic threshold.

3.1. Comments in variables and constants should describe their contents, not their purpose.


Earlier, I said that the name of a variable or constant should describe its purpose. But a comment to a variable or constant must describe the content , not the purpose .

 const randomNumber = 6 //     

In this example, the comment describes why randomNumber assigned the value 6 and where it came from. The comment does not describe where randomNumber will be used. Here are some more examples:

 const ( StatusContinue = 100 // RFC 7231, 6.2.1 StatusSwitchingProtocols = 101 // RFC 7231, 6.2.2 StatusProcessing = 102 // RFC 2518, 10.1 StatusOK = 200 // RFC 7231, 6.3.1 

In the context of HTTP, the number 100 known as StatusContinue , which is defined in RFC 7231, section 6.2.1.

Council For variables without an initial value, the comment should describe who is responsible for initializing this variable.

 // sizeCalculationDisabled ,   //     . . dowidth. var sizeCalculationDisabled bool 

Here the comment informs the reader that the dowidth function dowidth responsible for maintaining the state of sizeCalculationDisabled .

Council Hide in sight. This is advice from Kate Gregory . Sometimes the best name for a variable is hidden in the comments.

 //   SQL var registry = make(map[string]*sql.Driver) 

The comment was added by the author, because the name registry does not sufficiently explain its purpose - this is the registry, but what is the registry?

If you rename a variable in sqlDrivers, it becomes clear that it contains SQL drivers.

 var sqlDrivers = make(map[string]*sql.Driver) 

Now the comment has become redundant and can be deleted.

3.2. Always document public symbols.


Your package documentation is generated by godoc, so you should add a comment to each public symbol declared in the package: variable, constant, function, and method.

Here are two rules from the Google Style Guide:



 package ioutil // ReadAll   r      (EOF)   // ..    err == nil, not err == EOF. //  ReadAll     ,     //  . func ReadAll(r io.Reader) ([]byte, error) 

There is one exception to this rule: no need to document methods that implement the interface. Do not do this specifically:

 // Read   io.Reader func (r *FileReader) Read(buf []byte) (int, error) 

This comment says nothing. He does not say what the method does: worse, it sends somewhere to search for documentation. In this situation, I propose to completely delete the comment.

Here is an example from the io package.

 // LimitReader  Reader,    r, //    EOF  n . //   *LimitedReader. func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} } // LimitedReader   R,     //   N .   Read  N  //    . // Read  EOF,  N <= 0    R  EOF. type LimitedReader struct { R Reader // underlying reader N int64 // max bytes remaining } func (l *LimitedReader) Read(p []byte) (n int, err error) { if lN <= 0 { return 0, EOF } if int64(len(p)) > lN { p = p[0:lN] } n, err = lRRead(p) lN -= int64(n) return } 

Please note that the LimitedReader declaration is immediately preceded by the function that uses it, and the LimitedReader.Read declaration follows the LimitedReader.Read declaration itself. Although LimitedReader.Read itself is not documented, but it can be understood that this is an implementation of io.Reader .

Council Before writing a function, write a comment describing it. If you find it difficult to write a comment, then this is a sign that the code you are going to write will be difficult to understand.

3.2.1. Don't comment bad code, rewrite it


“Do not comment on the bad code - rewrite it” - Brian Kernigan

It is not enough to indicate in the comments the difficulty of the code fragment. If you are faced with one of these comments, you should start a ticket with a reminder of refactoring. You can live with technical debt as long as its amount is known.

In the standard library, it is customary to leave comments in the TODO style with the name of the user who noticed the problem.

 // TODO(dfc)  O(N^2),     . 

This is not an obligation to fix the problem, but the specified user may be the best person to contact with the question. Other projects accompany TODO with date or ticket number.

3.2.2. Instead of commenting the code, refactor it.


“Good code is the best documentation. When you are going to add a comment, ask yourself: “How to improve the code so that this comment is not needed?” Refactor and leave the commentary to make it clearer. ” - Steve McConnell

Functions should perform only one task. If you want to write a comment, because some fragment is not connected with the rest of the function, then consider the possibility of extracting it into a separate function.

Smaller functions are not only clearer, but they are easier to check separately from each other. When you isolated the code into a separate function, its name can replace the comment.

4. Package structure


“Write modest code: modules that do not show anything extra to other modules and that do not rely on the implementations of other modules” - Dave Thomas

Each package is essentially a separate, small Go program. As the implementation of a function or method does not matter for the caller, the implementation of the functions, methods, and types that make up the public API of your package also does not matter.

A good Go package tends to have minimal connectivity with other packages at the source code level so that as the project grows, changes in one package do not cascade across the entire code base. Such situations greatly inhibit programmers working on this code base.

In this section we will talk about package design, including its name and tips on writing methods and functions.

4.1. A good package starts with a good name.


Go . , .

, , . , : « ?» « X», « HTTP».

Council , .

4.1.1.


. , . , , :

  1. .
  2. . , .

4.2. base , common util


— , . . , , : .

utils helpers , , . - , . , - .

, utils helpers , , , . , , .

«[] , » —

, , .

Council . , strings .

base common , . , , , , .

, net/http client server , client.go server.go , transport.go .

Council , .

  • Get net/http http.Get .
  • The type Readerof the package stringswhen converted to other packages is converted to strings.Reader.
  • The interface Errorfrom the package is netclearly related to network errors.

4.3. Come back quickly, without diving


Since Go does not use exceptions in the control flow, there is no need to dig deep into the code to provide a top-level structure for tryand blocks catch. Instead of a multi-level hierarchy, the Go code goes down the screen as the function advances. My friend Mat Ryer calls this practice a “line of sight . ”

This is achieved with the help of boundary operators : conditional blocks with a precondition at the input to the function. Here is an example from the package bytes:

 func (b *Buffer) UnreadRune() error { if b.lastRead <= opInvalid { return errors.New("bytes.Buffer: UnreadRune: previous operation was not a successful ReadRune") } if b.off >= int(b.lastRead) { b.off -= int(b.lastRead) } b.lastRead = opInvalid return nil } 

Upon entering the function UnreadRune, the status is checked b.lastReadand if the previous operation was not ReadRune, an error is immediately returned. The rest of the function works on the assumption that b.lastReadmore than opInvalid.

Compare with the same function, but without the boundary operator:

 func (b *Buffer) UnreadRune() error { if b.lastRead > opInvalid { if b.off >= int(b.lastRead) { b.off -= int(b.lastRead) } b.lastRead = opInvalid return nil } return errors.New("bytes.Buffer: UnreadRune: previous operation was not a successful ReadRune") } 

The body of a more likely successful branch is embedded in the first condition if, and the condition for a successful exit return nilshould be detected by carefully matching the closing brackets. The last line of the function now returns an error, and you need to track the execution of the function to the corresponding opening bracket to find out how to get to this point.

This option is harder to read, which degrades the quality of programming and code support, so Go prefers to use boundary operators and return errors at an early stage.

4.4. Make a value of zero useful


, , , , . : — , — nil, , .

Go . , Go, : « ».

sync.Mutex , , . sync.Mutex. The code takes into account this fact, so that the type is suitable for use without explicit initialization.

 type MyInt struct { mu sync.Mutex val int } func main() { var i MyInt // i.mu is usable without explicit initialisation. i.mu.Lock() i.val++ i.mu.Unlock() } 

Another example of a type with a useful zero value is bytes.Buffer. You can declare and start writing to it without explicit initialization.

 func main() { var b bytes.Buffer b.WriteString("Hello, world!\n") io.Copy(os.Stdout, &b) } 

The zero value of this structure means that lenboth capare equal 0, and y array, a pointer to memory with the contents of the backup array of the slice, value nil. This means that you do not need to explicitly make a cut, you can simply declare it.

 func main() { // s := make([]string, 0) // s := []string{} var s []string s = append(s, "Hello") s = append(s, "world") fmt.Println(strings.Join(s, " ")) } 

Note . var s []stringsimilar to the two commented lines above, but not identical to them. There is a difference between the value of the slice, equal to nil, and the value of the slice, which has a zero length. The following code will print false.

 func main() { var s1 = []string{} var s2 []string fmt.Println(reflect.DeepEqual(s1, s2)) } 

, — nil — , nil. .

 type Config struct { path string } func (c *Config) Path() string { if c == nil { return "/usr/home" } return c.path } func main() { var c1 *Config var c2 = &Config{ path: "/export", } fmt.Println(c1.Path(), c2.Path()) } 

4.5.


— , .

, Go:

  1. , .
  2. .

Go , . , , : .

, ! , , . , , , .

, :

  1. Move the corresponding variables as fields to the structures that need them.
  2. Use interfaces to reduce the relationship between behavior and the implementation of this behavior.

5. Project structure


Let's talk about how packages are combined into a project. This is usually a single git repository.

Like a package, every project should have a clear goal. If it is a library, it should do one thing, for example, parsing XML or logging. You should not combine several goals in one project, it will help to avoid a terrible library common.

Council , common , (back-port fixes) common , , , API.

(-, Kubernetes . .), . , Kubernetes cmd/contour , , Kubernetes, .

5.1. ,


- , Go : .

Go : , Java ( public , protected , private default ). ++.

Go : , (/). , , Go.

. «» « » public private.

Given the limited access control options, which methods to use to avoid overly complex package hierarchies?

CouncilIn each package except cmd/and internal/must contain the source code.

I have repeatedly repeated that it is better to prefer a smaller number of packets of a larger size. Your default position should be to not create a new package. This leads to the fact that too many types become publicly available, creating a wide and small area of ​​the available API. Below is discussed in more detail this thesis.

CouncilCome with Java?

If you come from the world of Java or C #, then remember the unspoken rule: a Java package is equivalent to one source file .go. The Go package is equivalent to the whole Maven module or .NET assembly.

5.1.1. Organize code by files using import instructions


If you order packages by services, should you do the same for the files in the package? How to find out when to split one file .gointo several? How do I know that you have gone too far and need to think about merging files?

Here are the recommendations that I use:


Council .

. Go . ( — Go). .

5.1.2.


go testing . http2 , http2_test.go http2 . http2_test.go , http2 . .

go , test , http_test . , , , . , . .

I recommend using internal tests for unit tests of a package. This allows you to test each function or method directly, avoiding the bureaucracy of external testing.

But be sure to put into the external test file examples of test functions ( Example). This ensures that when viewed in godoc, the examples will receive the appropriate package prefix and can be easily copied.

Council , .

, , Go go . , net/http net .

.go , , .

5.1.3. , API


If there are multiple packages in your project, you may find exported functions that are intended for use by other packages, but not for the public API. In such a situation, the tool gorecognizes a special folder name internal/that can be used to place code that is open to your project but closed to others.

To create such a package, place it in a directory with the name internal/or in its subdirectory. When the team gosees the import package with the path internal, it checks the location of the caller in the directory or subdirectory internal/.

For example, a package .../a/b/c/internal/d/e/fcan import only a package from a directory tree .../a/b/c, but not .../a/b/gany other repository (seedocumentation ).

5.2. Main package minimum size


The function mainand package mainshould have minimal functionality, because it main.mainacts as a singleton: there can be only one function in the program main, including tests.

Since it main.mainis a singleton, there are many restrictions on the called objects: they are called only during main.mainor main.init, and only once . This makes it difficult to write tests for code main.main. Thus, one should strive to derive as much logic as possible from the main function and, ideally, from the main package.

Council func main() must analyze the flags, open connections with databases, loggers, etc., and then transfer the execution to a high-level object.

6. API structure


.

, , . . -.

API, , , : , .

API, , .

6.1. API,


«API » —

Josh Bloch's advice is perhaps the most valuable in this article. If the API is difficult to use for simple things, then each API call is more complicated than necessary. When an API call is complex and non-obvious, it is likely to be overlooked.

6.1.1. Be careful with functions that accept multiple parameters of the same type.


A good example of a simple at first glance, but difficult to use API, is when it requires two or more parameters of the same type. Compare two function signatures:

 func Max(a, b int) int func CopyFile(to, from string) error 

What is the difference between these two functions? Obviously, one returns a maximum of two numbers, and the other copies the file. But this is not the main thing.

 Max(8, 10) // 10 Max(10, 8) // 10 

Max is commutative : the order of the parameters does not matter. A maximum of eight and ten is ten, regardless of whether eight is compared to ten or ten or eight.

But in the case of CopyFile it is not.

 CopyFile("/tmp/backup", "presentation.md") CopyFile("presentation.md", "/tmp/backup") 

Which of these operators will back up your presentation, and which one will overwrite it with the version of last week? You cannot tell until you check the documentation. In the course of code review, it is not clear whether the order of the arguments is correct or not. Again, you need to look in the documentation.

One possible solution is the introduction of an auxiliary type responsible for the correct call CopyFile.

 type Source string func (src Source) CopyTo(dest string) error { return CopyFile(dest, string(src)) } func main() { var from Source = "presentation.md" from.CopyTo("/tmp/backup") } 

It is CopyFilealways called correctly here - this can be asserted using a unit test - and can be made private, which further reduces the likelihood of incorrect use.

Council An API with several parameters of the same type is difficult to use correctly.

6.2. Design an API for the main use case.


A few years ago, I gave a talk on the use of functional options to make the API easier by default.

The essence of the presentation was that an API should be developed for the main use case. In other words, the API should not require the user to provide extra parameters that do not interest him.

6.2.1. It is not recommended to use nil as a parameter.


I began by saying that you should not force the user to provide API parameters that do not interest him. This means designing APIs for the main use case (the default).

Here is an example from the net / http package.

 package http // ListenAndServe listens on the TCP network address addr and then calls // Serve with handler to handle requests on incoming connections. // Accepted connections are configured to enable TCP keep-alives. // // The handler is typically nil, in which case the DefaultServeMux is used. // // ListenAndServe always returns a non-nil error. func ListenAndServe(addr string, handler Handler) error { 

ListenAndServeIt takes two parameters: a TCP address to listen for incoming connections and http.Handlerto process an incoming HTTP request. Serveallows the second parameter to be nil. The comments indicate that usually the caller will actually pass nil, which indicates a desire to use http.DefaultServeMuxas an implicit parameter.

Now the caller Servehas two ways to do the same.

 http.ListenAndServe("0.0.0.0:8080", nil) http.ListenAndServe("0.0.0.0:8080", http.DefaultServeMux) 

.

nil . http http.Serve , ListenAndServe :

 func ListenAndServe(addr string, handler Handler) error { l, err := net.Listen("tcp", addr) if err != nil { return err } defer l.Close() return Serve(l, handler) } 

ListenAndServe nil , http.Serve . , http.Serve « nil , DefaultServeMux ». nil , nil . Serve

 http.Serve(nil, nil) 

.

Council nil nil .

http.ListenAndServe API , .

nil DefaultServeMux .

  const root = http.Dir("/htdocs") http.Handle("/", http.FileServer(root)) http.ListenAndServe("0.0.0.0:8080", nil) 



  const root = http.Dir("/htdocs") http.Handle("/", http.FileServer(root)) http.ListenAndServe("0.0.0.0:8080", http.DefaultServeMux) 

?

  const root = http.Dir("/htdocs") mux := http.NewServeMux() mux.Handle("/", http.FileServer(root)) http.ListenAndServe("0.0.0.0:8080", mux) 

Council , . , .

Council API- , . , , , .

6.2.2. []T


.

 func ShutdownVMs(ids []string) error 

, . , , . , , «» , .

, ids , , . , .

API, , , . :

 if svc.MaxConnections > 0 || svc.MaxPendingRequests > 0 || svc.MaxRequests > 0 || svc.MaxRetries > 0 { // apply the non zero parameters } 

if , . :

 // anyPostive indicates if any value is greater than zero. func anyPositive(values ...int) bool { for _, v := range values { if v > 0 { return true } } return false } 

, :

 if anyPositive(svc.MaxConnections, svc.MaxPendingRequests, svc.MaxRequests, svc.MaxRetries) { // apply the non zero parameters } 

anyPositive , - :

 if anyPositive() { ... } 

anyPositive false . . , anyPositive true .

, anyPositive, . (varargs):

 // anyPostive indicates if any value is greater than zero. func anyPositive(first int, rest ...int) bool { if first > 0 { return true } for _, v := range rest { if v > 0 { return true } } return false } 

anyPositive .

6.3. Let functions define the desired behavior.


Suppose I was given the task to write a function that preserves the structure Documenton disk.

 // Save      f. func Save(f *os.File, doc *Document) error 

Save , Document *os.File . .

Save . , , .

Save , . , , .

, f .

*os.File , Save , , , . , Save *os.File .

What can be done?

 // Save      // ReadWriterCloser. func Save(rwc io.ReadWriteCloser, doc *Document) error 

io.ReadWriteCloser — Save , .

, io.ReadWriteCloser , *os.File .

Save , , *os.File .

Save *os.File , io.ReadWriteCloser .

.

-, Save , , , — .

 // Save      // WriteCloser. func Save(wc io.WriteCloser, doc *Document) error 

Save .

-, Save , . , wc .

Save Close , .

, .

 // Save      // Writer. func Save(w io.Writer, doc *Document) error 

— Save io.Writer , , .

( , ), , Save , io.Writer .

7.


, .

, .

7.1. ,


, — .

. « ». , .

« » . « ». .

7.1.1.


.

 func CountLines(r io.Reader) (int, error) { var ( br = bufio.NewReader(r) lines int err error ) for { _, err = br.ReadString('\n') lines++ if err != nil { break } } if err != io.EOF { return 0, err } return lines, nil } 

, CountLines io.Reader , *os.File ; io.Reader , .

bufio.Reader , ReadString , , , .

, , . , :

  _, err = br.ReadString('\n') lines++ if err != nil { break } 

— .

, , , ReadString , , . , .

, , , .

. , ?

. ReadString io.EOF , . , ReadString - «, ». , CountLine , , io.EOF , , nil , .

, , . .

 func CountLines(r io.Reader) (int, error) { sc := bufio.NewScanner(r) lines := 0 for sc.Scan() { lines++ } return lines, sc.Err() } 

bufio.Scanner bufio.Reader .

bufio.Scanner bufio.Reader , , .

. bufio.Scanner , .

sc.Scan() true , . , for . , CountLines , .

-, sc.Scan false , for . bufio.Scanner , , sc.Err() , .

, sc.Err() io.EOF nil , .

Council , .

7.1.2. WriteResponse


« — » .

, , . , , , ioutil.ReadFile ioutil.WriteFile . -. . HTTP-, HTTP-.

 type Header struct { Key, Value string } type Status struct { Code int Reason string } func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error { _, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason) if err != nil { return err } for _, h := range headers { _, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value) if err != nil { return err } } if _, err := fmt.Fprint(w, "\r\n"); err != nil { return err } _, err = io.Copy(w, body) return err } 

fmt.Fprintf . , . , \r\n , . , io.Copy , , WriteResponse .

. , errWriter .

errWriter io.Writer , . errWriter . .

 type errWriter struct { io.Writer err error } func (e *errWriter) Write(buf []byte) (int, error) { if e.err != nil { return 0, e.err } var n int n, e.err = e.Writer.Write(buf) return n, nil } func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error { ew := &errWriter{Writer: w} fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason) for _, h := range headers { fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value) } fmt.Fprint(ew, "\r\n") io.Copy(ew, body) return ew.err } 

errWriter WriteResponse , . . ew.err , io.Copy.

7.2.


, , . .

 // WriteAll writes the contents of buf to the supplied writer. func WriteAll(w io.Writer, buf []byte) { w.Write(buf) } 

, . , w.WriteAll .

. , .

 func WriteAll(w io.Writer, buf []byte) error { _, err := w.Write(buf) if err != nil { log.Println("unable to write:", err) // annotated error goes to log file return err // unannotated error returned to caller } return nil } 

, w.Write , , , , , , .

, :

 func WriteConfig(w io.Writer, conf *Config) error { buf, err := json.Marshal(conf) if err != nil { log.Printf("could not marshal config: %v", err) return err } if err := WriteAll(w, buf); err != nil { log.Println("could not write config: %v", err) return err } return nil } 

, .

 unable to write: io.EOF could not write config: io.EOF 

- .

 err := WriteConfig(f, &conf) fmt.Println(err) // io.EOF 

, .

 func WriteConfig(w io.Writer, conf *Config) error { buf, err := json.Marshal(conf) if err != nil { log.Printf("could not marshal config: %v", err) // oops, forgot to return } if err := WriteAll(w, buf); err != nil { log.Println("could not write config: %v", err) return err } return nil } 

, . , Go , .

, , . - .

Go , . JSON , buf : , , , JSON.

, WriteAll . , , . , , — , JSON, .

7.2.1.


, . , .

fmt.Errorf .

 func WriteConfig(w io.Writer, conf *Config) error { buf, err := json.Marshal(conf) if err != nil { return fmt.Errorf("could not marshal config: %v", err) } if err := WriteAll(w, buf); err != nil { return fmt.Errorf("could not write config: %v", err) } return nil } func WriteAll(w io.Writer, buf []byte) error { _, err := w.Write(buf) if err != nil { return fmt.Errorf("write failed: %v", err) } return nil } 

, .

-, Error() - :

 could not write config: write failed: input/output error 

7.2.2. github.com/pkg/errors


fmt.Errorf , . , , , :

  1. , .
  2. .

, . - errors :

 func ReadFile(path string) ([]byte, error) { f, err := os.Open(path) if err != nil { return nil, errors.Wrap(err, "open failed") } defer f.Close() buf, err := ioutil.ReadAll(f) if err != nil { return nil, errors.Wrap(err, "read failed") } return buf, nil } func ReadConfig() ([]byte, error) { home := os.Getenv("HOME") config, err := ReadFile(filepath.Join(home, ".settings.xml")) return config, errors.WithMessage(err, "could not read config") } func main() { _, err := ReadConfig() if err != nil { fmt.Println(err) os.Exit(1) } } 

K&D:

 could not read config: open failed: open /Users/dfc/.settings.xml: no such file or directory 

.

 func main() { _, err := ReadConfig() if err != nil { fmt.Printf("original error: %T %v\n", errors.Cause(err), errors.Cause(err)) fmt.Printf("stack trace:\n%+v\n", err) os.Exit(1) } } 

, :

 original error: *os.PathError open /Users/dfc/.settings.xml: no such file or directory stack trace: open /Users/dfc/.settings.xml: no such file or directory open failed main.ReadFile /Users/dfc/devel/practical-go/src/errors/readfile2.go:16 main.ReadConfig /Users/dfc/devel/practical-go/src/errors/readfile2.go:29 main.main /Users/dfc/devel/practical-go/src/errors/readfile2.go:35 runtime.main /Users/dfc/go/src/runtime/proc.go:201 runtime.goexit /Users/dfc/go/src/runtime/asm_amd64.s:1333 could not read config 

errors , . , Go .

8.


Go - . , ( ) , Go , , . , Go.

Go , select go . Go , , . : , - , Go.

, Go — , . , . , , , .

Go.

8.1. -


?

 package main import ( "fmt" "log" "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello, GopherCon SG") }) go func() { if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) } }() for { } } 

, : -. , for{} main - main, -, , - .

Go , (live-lock).

? .

 package main import ( "fmt" "log" "net/http" "runtime" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello, GopherCon SG") }) go func() { if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) } }() for { runtime.Gosched() } } 

, , . .

Go, - .

 package main import ( "fmt" "log" "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello, GopherCon SG") }) go func() { if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) } }() select {} } 

select . , runtime.GoSched() . , .

, , , . , http.ListenAndServe -, - main, http.ListenAndServe -.

Council main.main , Go , -, .

 package main import ( "fmt" "log" "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello, GopherCon SG") }) if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) } } 

: - , , , .

, - .

Council Go -, . , .

8.2.


API?

 // ListDirectory returns the contents of dir. func ListDirectory(dir string) ([]string, error) 

 // ListDirectory returns a channel over which // directory entries will be published. When the list // of entries is exhausted, the channel will be closed. func ListDirectory(dir string) chan string 

: , , - . , ListDirectory , . , , .

. Go, ListDirectory , . , , . ListDirectory , - .

. -: , , , , . , .

ListDirectory :


: , .

 func ListDirectory(dir string, fn func(string)) 

filepath.WalkDir .

Council -, . .

8.3. -, ,


- . Go — . , , -.

http- : 8080 8001 /debug/pprof .

 package main import ( "fmt" "net/http" _ "net/http/pprof" ) func main() { mux := http.NewServeMux() mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) { fmt.Fprintln(resp, "Hello, QCon!") }) go http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux) // debug http.ListenAndServe("0.0.0.0:8080", mux) // app traffic } 

, .

, , .

 func serveApp() { mux := http.NewServeMux() mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) { fmt.Fprintln(resp, "Hello, QCon!") }) http.ListenAndServe("0.0.0.0:8080", mux) } func serveDebug() { http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux) } func main() { go serveDebug() serveApp() } 

serveApp serveDebug , main.main . , serveApp serveDebug .

. serveApp , main.main , .

Council Go , , . : .

serveDebug -, - , . , , /debug .

, -, .

 func serveApp() { mux := http.NewServeMux() mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) { fmt.Fprintln(resp, "Hello, QCon!") }) if err := http.ListenAndServe("0.0.0.0:8080", mux); err != nil { log.Fatal(err) } } func serveDebug() { if err := http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux); err != nil { log.Fatal(err) } } func main() { go serveDebug() go serveApp() select {} } 

serverApp serveDebug ListenAndServe log.Fatal . -, select{} .

:

  1. ListenAndServe nil , log.Fatal , HTTP .
  2. log.Fatal os.Exit , ; , - , . .

Council log.Fatal main.main init .

-, , , .

 func serveApp() error { mux := http.NewServeMux() mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) { fmt.Fprintln(resp, "Hello, QCon!") }) return http.ListenAndServe("0.0.0.0:8080", mux) } func serveDebug() error { return http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux) } func main() { done := make(chan error, 2) go func() { done <- serveDebug() }() go func() { done <- serveApp() }() for i := 0; i < cap(done); i++ { if err := <-done; err != nil { fmt.Println("error: %v", err) } } } 

- . -, , done , - .

done , for range , -. -, .

- , . - .

http.Server , . serve http.Handler , http.ListenAndServe , stop , Shutdown .

 func serve(addr string, handler http.Handler, stop <-chan struct{}) error { s := http.Server{ Addr: addr, Handler: handler, } go func() { <-stop // wait for stop signal s.Shutdown(context.Background()) }() return s.ListenAndServe() } func serveApp(stop <-chan struct{}) error { mux := http.NewServeMux() mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) { fmt.Fprintln(resp, "Hello, QCon!") }) return serve("0.0.0.0:8080", mux, stop) } func serveDebug(stop <-chan struct{}) error { return serve("127.0.0.1:8001", http.DefaultServeMux, stop) } func main() { done := make(chan error, 2) stop := make(chan struct{}) go func() { done <- serveDebug(stop) }() go func() { done <- serveApp(stop) }() var stopped bool for i := 0; i < cap(done); i++ { if err := <-done; err != nil { fmt.Println("error: %v", err) } if !stopped { stopped = true close(stop) } } } 

done stop , - http.Server . , - ListenAndServe . - , main.main .

Council — . - , ó .

Source: https://habr.com/ru/post/441842/


All Articles