This article is by Austin

Guiding principles

We’re going to talk about best practices in a programming language, so we should first define what “best” is. If you listened to my talk yesterday, you must have seen the words of Russ Cox from the Go team:

Software engineering is what happens when you add time or developers to your programming process. – Russ Cox.

Russ is talking about the difference between software “programming,” which is a program you write, and software “engineering,” which is a product that more people use for a long time. Software engineers come and go, teams grow and shrink, requirements change, new features are added, and bugs are fixed. This is the nature of software “engineering.”

I may have been one of the earliest users of Go in the room, but my claim today is not so much based on my qualifications as the guiding principle that really comes from the language itself, which is:

  1. simplicity
  2. readability
  3. productivity

You may have noticed that I didn’t mention performance or concurrency. There are actually quite a few languages that perform more efficiently than Go, but they’re certainly not as simple. Some languages have concurrency as their top goal, but they are not readable or productive. Performance and concurrency are important, but not as important as simplicity, readability, and productivity.

simplicity

Why should we strive for simplicity, and why is simplicity so important to Go programming?

There are so many times when we say, “I don’t understand this code,” right? We’re afraid to change a little bit of code, lest that little change cause something else to go wrong that you don’t understand, and you can’t fix it.

That’s complexity. Complexity makes readable programs unreadable, and complexity kills many software projects.

Simplicity is the ultimate goal of Go. Whatever program we write, we should all be able to agree that it should be simple.

readability

Readability is essential for maintainability. — Mark Reinhold, JVM Language Summit 2018 Readability is essential for maintainability.

Why is readability of Go code so important? Why should we strive for readability?

Programs must be written for people to read, And only incidentally for machines to execute. — Hal Abelson and Gerald Sussman Structure and Interpretation of Computer Programs should be written to be read by humans, but only incidentally implemented by machines.

Readability is so important to all programs — not just Go programs — because programs are written by humans and read by others, and actually executed by machines is secondary.

Code is read far more often than it is written. A small piece of code may be read hundreds or thousands of times over its lifetime.

The most important skill for a programmer is The ability to effectively communicate ideas. — Gaston Jorquera ^1 The most important skill for a programmer is the ability to communicate ideas effectively.

Readability is the key to figuring out what a program is doing. How can you maintain a program if you don’t know what it does? If a piece of software is unusable and maintained, it may be rewritten, and this may be the last time your company invests in GO.

If you’re just writing a program for yourself, maybe it’s one-off, or you’re the only one using it, you can write it however you want. But if the program is a collaborative contribution of many people, or if it is used by many people over a long period of time because it addresses their needs, meets certain features, and the environment in which it is run changes, then the maintainability of the program must be the goal.

The first step in writing maintainable programs is to make sure the code is readable.

productivity

Arranging Design is the art of working code to work today, and be changeable forever. And it can still be changed later.

The last fundamental principle I want to focus on is productivity. Developer productivity is a complex topic, but it boils down to this: how much time you waste with tools, external codebase, in order to work effectively. Go programmers should feel that they can benefit from a lot of things in their work. (Austin Luo: The implication is that Go’s toolset and base library are so complete that many things are at your fingertips.)

One joke is that Go was designed during compilation of C++ programs. Fast compilation is a key feature of Go that appeals to new developers. Compilation speed is still a constant battleground, and it’s fair to say that while other languages take minutes to compile, Go takes seconds. This helps Go developers be as efficient as dynamic language developers, but without the reliability problems of the dynamic language itself.

Go developers realize that code is written to be read and prioritize reading over writing. Go aims to force code to be written in a particular style from toolsets, habits, and so on, which removes barriers to learning project-specific terminology and helps developers spot potential errors simply by “looking” wrong.

Go developers don’t spend their days debugging unaccountable compilation errors. They don’t waste time on complex build scripts or deploying code into production. More importantly, they don’t spend time trying to make sense of code written by their colleagues.

When the Go language team talks about a language that must scale, they are talking about productivity.

identifier

The first topic we will discuss is identifiers. An identifier is a descriptor of a name, which can be the name of a variable, a function, a method, a type, a package, and so on.

Symptomatic of Poor naming is symptomatic of Poor design. — Dave Cheney’s bad name is symptomatic of bad design.

Given Go’s syntactic limitations, the names we choose for things in our program have a disproportionate impact on the readability of our program. Good readability is the key to code quality, so choosing a good name is critical to Go code readability.

Choose clear names, not concise ones

Obvious code is important. What you can do in one line you should do in three. We should split it into three lines.

Go is not a language that focuses on fine-tuning code to one line, nor is it a language that focuses on refining code to the minimum number of lines. We’re not looking for the source to take up less space on disk or how long it takes to input the code.

Good naming is like a Good joke. If you have to explain it, it’s not funny. Then it’s not funny.

The key to this clarity is the identifier we choose for the Go program. Let’s take a look at what makes a good name:

  • ** Good names are concise. ** A good name is not necessarily as short as possible, but it certainly does not waste anything irrelevant on it, and good names have a high signal-to-noise ratio.
  • ** Good names are descriptive. ** A good name should describe the use of a variable or constant, not its content. A good name should describe the result of a function or the behavior of a method, not the operation of the function or method itself. A good name should describe the purpose of a package, not its contents. The more accurately a name describes something, the better the name.
  • Good names are predictable. You should be able to infer from the name how it will be used, which is a function of choosing a descriptive name, while also following tradition. This is what the Go developers are talking about when they talk about idioms.

Let’s talk more about it.

Identifier length

The Go style is sometimes criticized for recommending short variable names. As Rob Pike said, “Go developers want identifiers of the right length”. ^ 1

Andrew Gerrand suggests using longer identifiers to signal to readers that they are of higher importance.

The greater The distance between a name’s declaration and its uses, The longer the name should be. — Andrew Gerrand ^2 The farther apart identifiers are declared and used, the longer the name should be.

From this, we can generalize some guidelines:

  • Short variable names work well when the distance between declaration and last use is small.
  • Long variable names need to be justified differently: the longer the variable name, the more justification is needed. Long, verbose names carry very little information compared to their weight on the page.
  • Do not include the name of a variable’s type in its name.
  • Constants need to describe the meaning of the value they store, not how they are used.
  • Single-letter variables can be used for loops or logical branches, word variables can be used for parameters or return values, and multi-word phrases can be used for declarations at the function and package level.
  • Words can be used for methods, interfaces, and packages
  • Keep in mind that the name of the package will be the name the user uses to refer to it, making sure it makes more sense.

Let’s look at 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
}
Copy the code

In this example, the scope variable p is only used on the next line after it is defined. P exists only for a short time in the entire page of source code and in the execution of functions. Readers interested in P need only look at two lines of code.

In contrast, the variable people is defined in the function argument and exists in seven lines, as well as sum and count, which use longer names and require the reader to focus on a wider range of lines of code.

I could have used S instead of sum, and C (or n) instead of count, but that would have concentrated the variables on the same importance throughout the program. I could also use p instead of people, but there’s another problem with that: for… What are the variables in the range loop? The singular person also looks odd, with a very short lifetime name that is longer than the value from which it was derived.

Austin Luo: If the array people has a variable name p, then every element fetched from the array would be called a problem. For example, using Person, even using Person would seem strange because it is singular, and the lifetime of person is only two lines. Naming p (people) is longer than life. Tip: Use empty lines to segment a function’s execution just as you segment a document with empty lines. There are three operations in order in the function AverageAge. The first one is the prerequisite, checking that we don’t divide by zero when people is empty, the second one is summing up and counting, and the last one is calculating the average.

Context is key

It’s important to realize that the vast majority of naming suggestions are contextual. I like to call it a principle, not a rule.

What is the difference between the identifiers I and index? It’s hard to say with certainty that one is better than the other, as in:

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

The above code is generally considered to be more readable than the following:

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

But I disagree. Since both I and index are limited to the body of the for loop, the longer naming doesn’t make this code any easier to understand.

Anyway, which of the following two pieces of code is more readable?

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

or

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

In this example, OID is an abbreviation for the SNMP object ID, so writing it as O means that developers must interpret the regular symbol transformations they see in documentation as shorter symbols in code. Similarly, shortening index to I reduces its meaning as an index for SNMP messages.

Tip: Don’t mix long and short naming styles in parameter declarations.

Do not include the name of the type you belong to

Just as you would name a pet, you would name a dog “woof” and a cat “Mimi”, but not “woof Dog” or “Mimi Cat”. For the same reason, you should not include the name of a variable’s type in its name.

Variable names should reflect their content, not their type. Let’s look at this example:

var usersMap map[string]*User
Copy the code

What’s the advantage of naming this way? It’s probably good to know that it’s a map and that it’s related to the *User type. But because Go is a statically typed language, it doesn’t allow us to accidentally use scalar variables where we need them, so the Map suffix is actually redundant.

Now let’s see what happens when we define a variable like this:

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

We now have three map-type variables in this scope: usersMap, companiesMap, and productsMap, all of which map from strings to different types. We know that they are both maps, and we know that their map declarations prevent us from using one instead of the other — if we try to use companiesMap where we need map[string]*User, the compiler will throw an error. In this case, it’s clear that the Map suffix is not going to improve the clarity of your code, it’s just redundant stuff you type in when you program. (Austin Luo: Old way of thinking)

My advice is to avoid assigning any type suffix to variables.

Tip: If users cannot be clearly described, usersMap must not be either.

This advice also applies to function arguments, such as:

type Config struct {
  //
}

func WriteConfig(w io.Writer, config *Config)
Copy the code

It is superfluous to name the *Config parameter Config; we know it is *Config, as stated clearly in the function signature.

In this case it is recommended to consider conf or C — if the lifecycle is short enough.

If there is more than one *Config ina range, the names conf1 and conf2 are less descriptive than original and updated, and the latter are less error-prone than the former.

NOTE: Don’t let the package name usurp a more appropriate variable name. An imported identifier contains the name of the package to which it belongs. For example, we know that context. context is the type context in the package context. As a result, we can no longer use context as a variable or type name in our own packages. Func WriteLog(Context context. context, message String) This does not compile. This is why variables of type context. context are usually named CTX, as in func WriteLog(CTX context. context, message String).

Use a consistent naming style

Another thing about a good name is that it should be predictable. Readers should be able to understand how it works the first time they see it. If they come across a common name, they should be able to assume that it hasn’t changed its meaning since the last time they saw it.

For example, if you are passing a database handle, make sure the parameter names are the same each time. DB, dbase *sql.DB, DB *sql.DB, and database *sql.DB

db *sql.DB
Copy the code

Doing so increases familiarity: if you see DB, you know that it is * SQL.db and has been defined locally or provided by the caller.

Similarly for method receivers, use the same receiver name in each method of the type to make it easier for readers to make subjective inferences when reading and understanding across methods.

Austin Luo: “Receiver” is a special type of parameter. ^2 For example, func (b *Buffer) Read(p []byte) (n int, err Error) is usually represented by one or two letters, but should still be consistent between methods. Note: The short naming convention for receivers in Go is inconsistent with the recommendations currently provided. This was just one of the early choices made and has become the preferred style, like using CamelCase instead of snake_case. Tip: Go’s naming style specifies that the receiver has an acronym for a single letter name or a type derived from it. Sometimes you might find that the name of the receiver sometimes conflicts with the name of the parameter in the method, in which case, consider making the parameter name slightly longer, and still don’t forget to use the new name consistently.

Finally, certain single-letter variables are traditionally associated with loops and counts. For example, I, j, and k are often simple for loop variables. N is usually associated with a counter or accumulator. V is usually shorthand for a value, k is often used for a mapped key, and S is often used as shorthand for a string parameter.

As in the DB example above, the programmer expects I to be a loop variable. If you ensure that I is always a loop variable — and not used outside of the for loop — then when readers encounter a variable named I or j, they know that they are still in the loop.

Tip: If you find yourself running out of I, j, and k in a nested loop, it’s obvious that it’s time to split the function into smaller pieces.

Use a consistent declarative style

There are at least 6 ways to declare variables in Go (Austin Luo: Author says 6, but only lists 5)

  • var x int = 1
  • var x = 1
  • var x int; x = 1
  • var x = int(1)
  • x := 1

I’m sure there’s more I haven’t thought of. This is where Go’s designers realized there might have been a mistake, but now it’s too late to change it. With so many different ways to declare variables, how do we keep each Go programmer from choosing his or her own unique declaration style?

I want to show you some tips for declaring variables in my own programs. This is the style I use whenever POSSIBLE.

  • Only declare, not initialize, use *var* * *. ** will be explicitly initialized after the declarationvarThe keyword.

Copy the code

var players int // 0

var things []Thing // an empty slice of Things

var thing Thing // empty Thing struct

json.Unmarshall(reader, &thing)


Copy the code

The var keyword indicates that the variable was intentionally declared to have a zero value for the type. This is also consistent with the requirement to use var instead of short declaration syntax (Austin Luo: :=) when declaring variables at the package level — although I’ll say later that you shouldn’t use package-level variables at all.

  • For both declaration and initialization, use *: =* * *. ** WHEN we want to declare and initialize variables at the same time, in other words we don’t want variables to be initialized implicitly to zero, I recommend using the form of the short declaration syntax. This makes it clear to the reader: =The variables on the left are initialized on purpose.

To explain why, let’s go back to the above example, but this time each variable is intentionally initialized:

var players int = 0

var things []Thing = nil

var thing *Thing = new(Thing)
json.Unmarshall(reader, thing)
Copy the code

In the first and third examples, because Go has no automatic conversion from one type to another, the types to the left and right of the assignment operators must be consistent. The compiler can infer the type of the variable declared on the left from the type on the right. This example can be written more succinctly as follows:

var players = 0

var things []Thing = nil

var thing = new(Thing)
json.Unmarshall(reader, thing)
Copy the code

Since 0 is a zero value for players, it is unnecessary to explicitly initialize 0 for players. So to make it clear that we are using zero, we should write it like this:

var players int
Copy the code

What about the second statement? We cannot ignore the type as:

var things = nil
Copy the code

Because nil has no type ^2 at all. Instead, we have a choice, do we want to slice the zero value?

var things []Thing
Copy the code

Or do we want to create a slice with no elements?

var things = make([]Thing, 0)
Copy the code

If we want the latter, which is not a slice-type zero, then we should use short declarative syntax to make our choice clear to the reader:

things := make([]Thing, 0)
Copy the code

This tells the reader that we explicitly initialized Things.

Consider the third statement:

var thing = new(Thing)
Copy the code

This both explicitly initializes variables and introduces the new keyword, which Go programmers dislike and rarely use. If we follow the advice of short naming syntax, this sentence will become:

thing := new(Thing)
Copy the code

This makes it clear that thing is explicitly initialized as the result of new(thing) — a pointer to thing — but still retains the new that we don’t use very often. We can solve this problem by using the form of compact initialization,

thing := &Thing{}
Copy the code

This is the same Thing new(Thing) did — and many Go programmers are uncomfortable with this repetition. However, this statement still means that we explicitly initialized a pointer to thing {} — a zero value of thing.

Here, we should realize that thing is initialized to zero and its pointer address is passed to json.unmarshall:

var thing Thing
json.Unmarshall(reader, &thing)
Copy the code

Note: Of course, there are exceptions to any rule of thumb. For example, if some variables are closely related, it would be more readable to write min, Max := 0, 1000 rather than var min int Max := 1000

To sum up:

  • Used when only declared and not initializedvar.
  • Used when both declared and explicitly initialized: =.

Tip: Make clever statements more obvious. When something is complicated, make it seem complicated. Var Length uint32 = 0x80 Where length may be used with a library that requires a specific number type, and length is explicitly specified as a uint32 rather than just a short declaration: Length := uint32(0x80) In the first example, I deliberately violated the rule of using var declarations and explicit initializers. This decision, which is different from my usual form, makes the reader aware of the need for attention here.

Become a team player

I talked about the goal of software engineering, which is to produce readable, maintainable code. And you’ve probably spent most of your career working on projects where you’re not the only author. My advice in this case is to follow the team style.

Changing encoding styles in the middle of a file is not appropriate. Again, even if you don’t like it, maintainability is worth a lot more than your personal preference. My rule is that if goFMT is satisfied, it’s usually not worth doing a code style review.

Tip: If you’re renaming across the entire code base, don’t mix in other changes. If other people are using Git Bisect, they won’t want to have to wade through thousands of lines of code renaming to find your other changes.

Code comments

Before we move on to the larger topic, I want to talk for a few minutes about comments.

Good code has lots of comments, bad code requires lots of comments. — Dave Thomas and Andrew Hunt, The Pragmatic Programmer good code has lots of comments attached, bad code lacks lots of comments.

Code comments are critical to the readability of Go programs. A comment should do at least one of three things:

  1. Comments should explain what to do.
  2. Comments should explain “how.”
  3. The comment should explain the “why”.

The first form is suitable for public notation:

// Open opens the named file for reading.
// If successful, methods on the returned file can be used for reading.
Copy the code

The second form is suitable for comments within a method:

// queue all dependant actions
var results []chan error
for _, dep := range a.Deps {
        results = append(results, execute(seen, dep))
}
Copy the code

The third form, “Why do you do it”, is unique and cannot be replaced by or supersede the first two. The third type of comment is used to explain more situations that are difficult to take out of context and would otherwise be meaningless.

return &v2.Cluster_CommonLbConfig{
  // Disable HealthyPanicThreshold
  HealthyPanicThreshold: &envoy_type.Percent{
    Value: 0,}}Copy the code

In this example, it is not immediately clear what impact setting the percentage of HealthyPanicThreshold to zero would have. The comment is used to make it clear that setting the value to 0 actually disables the behavior of the panic threshold.

Comments on variables and constants should describe their content, not their purpose

I mentioned earlier that the name of a variable or constant should describe its purpose. When you add comments to a variable or constant, you should describe the content of the variable, not define its purpose.

const randomNumber = 6 // determined from an unbiased die
Copy the code

The comments for this example describe “why” randomNumber was assigned to 6, and also explain where the 6 value came from. But it 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
Copy the code

As defined in section 6.2.1 of RFC 7231, 100 is referred to as a StatusContinue in HTTP contexts.

Tips: For variables that have no initial value, Comments should describe who will be responsible for initializing them // sizeCalculationDisabled Indicates Whether it is safe // to calculate Types’ widths and alignments. Var sizeCalculationDisabled bool DoWidth is responsible for maintaining sizeCalculationDisabled. Kate Gregory mentioned that sometimes a good name can omit unnecessary comments. Var registry = make(mapStringsql.driver) var registry = make(mapStringsql.driver) By renaming the variable sqlDrivers, it is now clear that the purpose of this variable is to store SQL drivers. Var sqlDrivers = make(mapStringsql.driver)

Always document public symbols

Because GoDoc will serve as documentation for your package, you should always write comments for every public symbol — including variables, constants, functions, and methods — defined in your package.

Here are the two rules of the Go Style guide:

  • Any common functionality that is neither obvious nor short must be commented out.
  • Any function in the library must be annotated, regardless of length or complexity.
package ioutil

// ReadAll reads from r until an error or EOF and returns the data it read.
// A successful call returns err == nil, not err == EOF. Because ReadAll is
// defined to read from src until EOF, it does not treat an EOF from Read
// as an error to be reported.
func ReadAll(r io.Reader) ([]byte, error)
Copy the code

There is one exception to this rule: you do not need to document the methods that implement the interface, especially not:

// Read implements the io.Reader interface
func (r *FileReader) Read(buf []byte) (int, error)
Copy the code

This comment doesn’t say anything, it doesn’t tell you what the method does, and in fact, worse, it tells you to find documentation somewhere else. In this case I recommend removing the comment altogether.

Here is an example from the IO package:

// LimitReader returns a Reader that reads from r
// but stops with EOF after n bytes.
// The underlying implementation is a *LimitedReader.
func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} }

// A LimitedReader reads from R but limits the amount of
// data returned to just N bytes. Each call to Read
// updates N to reflect the new amount remaining.
// Read returns EOF when N <= 0 or when the underlying R returns EOF.
type LimitedReader struct {
  R Reader // underlying reader
  N int64  // max bytes remaining
}

func (l *LimitedReader) Read(p []byte) (n int, err error) {
  if l.N <= 0 {
    return 0, EOF
  }
  if int64(len(p)) > l.N {
    p = p[0:l.N]
  }
  n, err = l.R.Read(p)
  l.N -= int64(n)
  return
}
Copy the code

Note that LimitedReader is declared immediately after the function that uses it, and limitedReader. Read is defined immediately after LimitedReader, even though LimitedReader. That makes it clear that it is an implementation of IO.Reader.

Tip: Write a comment describing the function before you write it. If you find it difficult to write, it is a good indication that the code you are trying to write must be difficult to understand.

Don’t comment bad code, rewrite it

Don’t comment bad code — rewrite it — Brian Kernighan

It’s not enough to re-comment a crappy snippet of code; if you come across one, you should issue it to remember to refactor it later. Technical debt is fine as long as it is not excessive.

The convention in the standard library is to annotate a todo-style comment indicating who found the bad code.

// TODO(dfc) this is O(N^2), find a faster way to do this.
Copy the code

The name in the comment does not promise to fix the problem, but he may be the best person to fix it. Other notes usually include the date or question number.

Instead of commenting a chunk of code, refactor it

Good code is its own best documentation. As you’re about to add a comment, ask yourself, ‘How can I improve the code so that this comment isn’t needed? ‘Improve the code and then document it to make it even good. Before you’re ready to add a line of comments, ask yourself, “How can I improve this code so that it doesn’t need comments?” Optimize the code, then comment it to make it clearer.

The function should only do one thing. If you find that a piece of code needs to be commented out because it doesn’t relate to the rest of the function, consider splitting the code into separate functions.

In addition to being easier to understand, smaller functions are easier to test individually, and now that you’ve isolated unrelated code into separate functions, you expect that only the function name will be the only documentation you need.

This article has been authorized by the author to Tencent Cloud + community, more original text pleaseClick on the

Search concern public number “cloud plus community”, the first time to obtain technical dry goods, after concern reply 1024 send you a technical course gift package!