Introduction

Go, a new language, differs from its predecessors despite shared ideas. Translating C++ or Java code directly to Go often fails, as Java is not Go. However, approaching problems from a Go perspective can lead to successful, albeit different, programs. To excel in Go, grasp its unique properties and idioms. Familiarize yourself with Go's naming, formatting, and construction conventions, ensuring your code is understandable to other Go programmers.

Formatting

Go uses the gofmt tool to standardize formatting, automatically adjusting code to a consistent style. This approach eliminates the need for a prescriptive style guide and reduces time spent on formatting debates. By using gofmt, developers can focus on functionality, adjusting their code when necessary to align with the tool's output. This streamlined process enhances efficiency and promotes consistency across Go projects.

Consider a situation where you're writing a Go program and you're unsure about how to format a function with multiple return values.

Instead of manually adjusting the spacing or alignment of your return values, you can use gofmt, the Go formatter. It's designed to handle such situations and will automatically format your code according to the standard Go format.

For instance, given the function: "

func calculate(x, y int) (result int, errorMessage string) {
if y == 0 {
  errorMessage = "Error: Division by zero"
  return
 }
result = x / y
return
      }

Running gofmt will adjust the spacing around the return statement to ensure consistent formatting. This way, you can focus on the logic of your program without worrying about the minor details of formatting.

Commentary

Go supports both C-style /* */ block comments and C++-style // line comments, with the latter being more common. Block comments are useful within expressions or to comment out large sections of code, and are often used as package comments. Comments before top-level declarations are considered doc comments, which are the primary source of documentation for a Go package or command. To learn more about doc comments, refer to "Go Doc Comments".

Names

In Go, a name's first character determines its visibility: uppercase for external use, lowercase for internal. This is about syntax, not style. For exported methods, use camel-case starting with a capital, like WriteToDB. For non-English identifiers, you can prefix with 'X' to indicate exportedness, such as 'X日本語'. Abbreviations should maintain the same case..

Package names

When a package is imported, the package name becomes an accessor for the contents.

import "bytes"

When naming a package that deals with bytes.Buffer, it's important to choose a short, concise, and evocative name, following the convention of lowercase, single-word names. This is because anyone using your package will be typing this name. However, you don't need to worry about collisions initially, as package names are only the default for imports and can be changed locally in the importing package if a conflict arises. The specific file name in the import statement will clarify which package is being used, making confusion rare.

Another convention is that the package name is the base name of its source directory; the package in src/encoding/base64 is imported as "encoding/base64" but has name base64, not encoding_base64 and not encodingBase64.

The importer of a package will use the name to refer to its contents, so exported names in the package can use that fact to avoid repetition. (Don't use the import . notation, which can simplify tests that must run outside the package they are testing, but should otherwise be avoided.) For instance, the buffered reader type in the bufio package is called Reader, not BufReader, because users see it as bufio.Reader, which is a clear, concise name. Moreover, because imported entities are always addressed with their package name, bufio.Reader does not conflict with io.Reader. Similarly, the function to make new instances of ring.Ring—which is the definition of a constructor in Go—would normally be called NewRing, but since Ring is the only type exported by the package, and since the package is called ring, it's called just New, which clients of the package see as ring.New. Use the package structure to help you choose good names.

Another short example is once.Do; once.Do(setup) reads well and would not be improved by writing once.DoOrWaitUntilDone(setup). Long names don't automatically make things more readable. A helpful doc comment can often be more valuable than an extra long name.

Getters

Go does not automatically generate getters and setters. While it's acceptable to create them manually, it's not idiomatic to prefix "Get" to getter names. For instance, a field named "owner" should have a getter named "Owner" (exported in uppercase), not "GetOwner". Using uppercase for exported names distinguishes fields from methods. Similarly, a setter function would typically be named "SetOwner". These naming conventions enhance code readability and clarity in Go.

owner := obj.Owner()
if owner != user {
  obj.SetOwner(user)
}

Interface names

By convention, one-method interfaces are named by the method name plus an -er suffix or similar modification to construct an agent noun: Reader, Writer, Formatter, CloseNotifier etc.

Respect established function names like Read, Write, Close, Flush, and String, which have specific signatures and meanings. Avoid using these names for your methods unless they match the exact functionality. Conversely, if your type's method mirrors a well-known type's method, use the same name and signature; for example, name your string-converter method String, not ToString, to maintain clarity and consistency.

MixedCaps

Finally, the convention in Go is to use MixedCaps or mixedCaps rather than underscores to write multiword names.

Semicolons

Like C, Go's formal grammar terminates statements with semicolons, but unlike C, these semicolons are not explicitly typed in the source code. Instead, Go's lexer inserts semicolons automatically based on a straightforward rule to keep the input text mostly free of them.

The rule is: if the last token before a newline is an identifier (which includes words like `int` and `float64`), a basic literal (like a number or string constant), or one of certain tokens

break continue fallthrough return ++ -- ) }

the lexer always inserts a semicolon after the token. This could be summarized as, “if the newline comes after a token that could end a statement, insert a semicolon”.

A semicolon can also be omitted immediately before a closing brace, so a statement such as

go func() { for { dst <- <-src } }()

needs no semicolons. Idiomatic Go programs have semicolons only in places such as for loop clauses, to separate the initializer, condition, and continuation elements.

One consequence of the semicolon insertion rules is that you cannot put the opening brace of a control structure (if, for, switch, or select) on the next line. If you do, a semicolon will be inserted before the brace, which could cause unwanted effects. Write them like this

if i < f() {
  g()
}

not like this

if i < f()  // wrong!
{           // wrong!
    g()
}
Control structures

Go's control structures resemble C's but with notable differences: no do or while loops; a generalized for loop; a more flexible switch statement; optional initialization in if and switch statements; labeled break and continue; and new structures like type switches and select for multiway communications. Syntax differs in requiring brace-delimited bodies and omitting parentheses around conditions.

If

In Go a simple if looks like this:

if x > 0 {
    return y
  }

Mandatory braces encourage writing simple if statements on multiple lines. It's good style to do so anyway, especially when the body contains a control statement such as a return or break.

Since if and switch accept an initialization statement, it's common to see one used to set up a local variable.

if err := file.Chmod(0664); err != nil {
    log.Print(err)
    return err
}

In the Go libraries, you'll find that when an if statement doesn't flow into the next statement—that is, the body ends in break, continue, goto, or return—the unnecessary else is omitted.

f, err := os.Open(name)
if err != nil {
      return err
}
codeUsing(f)

Redeclaration and reassignment

In a := declaration a variable v may appear even if it has already been declared, provided:

  • this declaration is in the same scope as the existing declaration of v (if v is already declared in an outer scope, the declaration will create a new variable §),
  • the corresponding value in the initialization is assignable to v, and
  • there is at least one other variable that is created by the declaration.
  • The type of v is inferred from the initialization expression, if present.
  • The scope of v is limited to the block in which it is declared.

This unusual property is pure pragmatism, making it easy to use a single err value, for example, in a long if-else chain. You'll see it used often.

For

The Go for loop combines aspects of both for and while loops from C, though it lacks a do-while variant. It offers three forms, with only one requiring semicolons.

// Like a C for
for init; condition; post { }
            
// Like a C while
for condition { }
            
// Like a C for(;;)
for { }

Short declarations simplify index variable declaration directly within the loop.

sum := 0
for i := 0; i < 10; i++ {
    sum += i
}

If iterating over an array, slice, string, map, or reading from a channel, a range clause simplifies loop management.

for key, value := range oldMap {
  newMap[key] = value
}

If you only require the first item in the range (the key or index), omit the second item.

for key := range m {
    if key.expired() {
        delete(m, key)
    }
}

If you only require the second item in the range (the value), use the underscore (_) to discard the first.

sum := 0
for _, value := range array {
    sum += value
}

For strings, the range simplifies iteration by parsing UTF-8 and handling erroneous encodings, substituting them with the replacement rune U+FFFD. The loop

for pos, char := range "日本\x80語" { // \x80 is an illegal UTF-8 encoding
  fmt.Printf("character %#U starts at byte position %d\n", char, pos)
}

prints

character U+65E5 '日' starts at byte position 0
character U+672C '本' starts at byte position 3
character U+FFFD '�' starts at byte position 6
character U+8A9E '語' starts at byte position 7

In Go, there is no comma operator, and ++ and -- are statements rather than expressions. Therefore, if you need to handle multiple variables in a for loop, you should utilize parallel assignment, which does not support ++ and -- operations.

// Reverse a
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
    a[i], a[j] = a[j], a[i]
}

Switch

Go's switch statement is more versatile than C's. It accepts expressions that can be non-constants and non-integers. Cases are evaluated sequentially until a match is found, and if the switch lacks an expression, it defaults to switching on true. This allows for writing if-else-if-else chains as switch statements, which is both possible and idiomatic in Go.

func unhex(c byte) byte {
    switch {
    case '0' <= c && c <= '9':
        return c - '0'
    case 'a' <= c && c <= 'f':
        return c - 'a' + 10
    case 'A' <= c && c <= 'F':
        return c - 'A' + 10
    }
    return 0
}

There is no automatic fall-through in Go's switch statements, but cases can be presented in comma-separated lists.

func shouldEscape(c byte) bool {
    switch c {
    case ' ', '?', '&', '=', '#', '+', '%':
        return true
    }
    return false
}

While break statements in Go are less common than in some other C-like languages, they can terminate a switch statement early. However, there are situations where you may need to break out of a surrounding loop instead of the switch statement. In Go, this can be achieved by labeling the loop and using "break" with that label. The following example demonstrates both uses.

Loop:
    for n := 0; n < len(src); n += size {
        switch {
        case src[n] < sizeOne:
            if validateOnly {
                break
            }
            size = 1
            update(src[n])

        case src[n] < sizeTwo:
            if n+1 >= len(src) {
                err = errShortInput
                break Loop
            }
            if validateOnly {
                break
            }
            size = 2
            update(src[n] + src[n+1]<

Type switch

A switch in Go can determine the dynamic type of an interface variable using a type switch, which resembles a type assertion with the keyword type inside parentheses. When declaring a variable in the switch expression, this variable will hold the respective type in each case. It's common practice to reuse the variable name in such scenarios, effectively creating a new variable with the same name but a different type in each case.

var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
    fmt.Printf("unexpected type %T\n", t)     // %T prints whatever type t has
case bool:
    fmt.Printf("boolean %t\n", t)             // t has type bool
case int:
    fmt.Printf("integer %d\n", t)             // t has type int
case *bool:
    fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
    fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}
Functions

Multiple return values

One distinctive feature of Go is its ability for functions and methods to return multiple values. This feature improves upon several cumbersome practices seen in C programming: handling in-band error returns like -1 for EOF and modifying arguments passed by address.

In C, write errors are often indicated by a negative count, with the error code hidden in a mutable location. In Go, the Write function can return both a count and an error, providing explicit feedback like: "You wrote some bytes but not all, due to device capacity." The signature of the Write method on files from package os is:

func (file *File) Write(b []byte) (n int, err error)

In Go, functions often return both the number of bytes written and a non-nil error when n does not equal len(b), as documented. This approach aligns with common error-handling practices outlined in the documentation. Similarly, this method eliminates the necessity to pass a pointer for a return value to mimic a reference parameter. For instance, consider a straightforward function that retrieves a number from a specified position in a byte slice and returns both the number and the subsequent position.

func nextInt(b []byte, i int) (int, int) {
    for ; i < len(b) && !isDigit(b[i]); i++ {
    }
    x := 0
    for ; i < len(b) && isDigit(b[i]); i++ {
        x = x*10 + int(b[i]) - '0'
    }
    return x, i
}

You could use it to scan the numbers in an input slice b like this:

    for i := 0; i < len(b); {
        x, i = nextInt(b, i)
        fmt.Println(x)
    }

Named result parameters

In Go, function return values can be named and treated like regular variables, akin to incoming parameters. Named return values are initialized to their type's zero values when the function starts. If a return statement lacks arguments, the current values of these named return values are used for the function's result. While naming return values isn't obligatory, it enhances code conciseness and clarity, serving as documentation. For instance, naming the results of a function like nextInt clarifies the purpose of each returned integer.

func nextInt(b []byte, pos int) (value, nextPos int) {

Because named results are initialized and tied to an unadorned return, they can simplify as well as clarify. Here's a version of io.ReadFull that uses them well:

func ReadFull(r Reader, buf []byte) (n int, err error) {
    for len(buf) > 0 && err == nil {
        var nr int
        nr, err = r.Read(buf)
        n += nr
        buf = buf[nr:]
    }
    return
}

Defer

Go's defer statement schedules a function call, known as a deferred function, to execute just before the surrounding function returns. This feature is unconventional yet powerful, particularly useful for tasks like releasing resources such as unlocking a mutex or closing a file, ensuring they are handled irrespective of the function's return path.

// Contents returns the file's contents as a string.
func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
        return "", err
    }
    defer f.Close()  // f.Close will run when we're finished.

    var result []byte
    buf := make([]byte, 100)
    for {
        n, err := f.Read(buf[0:])
        result = append(result, buf[0:n]...) // append is discussed later.
        if err != nil {
            if err == io.EOF {
                break
            }
            return "", err  // f will be closed if we return here.
        }
    }
    return string(result), nil // f will be closed if we return here.
}

Deferring a call to a function like Close offers two key benefits. Firstly, it ensures the file is always closed, preventing the common mistake of forgetting to close it when adding new return paths. Secondly, it improves code clarity by placing the close operation near the open operation, rather than at the function's end.

The arguments passed to a deferred function, including the receiver in case of a method, are evaluated when the defer statement is executed, not when the function call actually happens. This avoids concerns about variables changing during function execution and allows a single defer statement to defer multiple function executions. Here's a silly example.

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}

Deferred functions execute in Last In, First Out (LIFO) order, which means that in this code, the numbers 4 3 2 1 0 will be printed when the function returns. A practical application is tracing function execution within a program. For instance, we can define simple tracing routines like these:

func trace(s string)   { fmt.Println("entering:", s) }
func untrace(s string) { fmt.Println("leaving:", s) }

// Use them like this:
func a() {
    trace("a")
    defer untrace("a")
    // do something....
}

We can do better by exploiting the fact that arguments to deferred functions are evaluated when the defer executes. The tracing routine can set up the argument to the untracing routine. This example:

func trace(s string) string {
    fmt.Println("entering:", s)
    return s
}

func un(s string) {
    fmt.Println("leaving:", s)
}

func a() {
    defer un(trace("a"))
    fmt.Println("in a")
}

func b() {
    defer un(trace("b"))
    fmt.Println("in b")
    a()
}

func main() {
    b()
}

prints

entering: b
in b
entering: a
in a
leaving: a
leaving: b

Defer in Go diverges from block-level resource management in other languages, offering unique and powerful applications rooted in its function-based nature.

Reference

ALL the documentation in this page is taken from go.dev