The Go Programming Language

This post is meant to record the fundamentals of Golang. The main material I used for this post is “The Go Programming Language”, which is the well-known tutorial book for the Golang beginner. Apart from that, I’ll lookup the topic that I’m confused on Internet.

Chapter 2 Program Structure

The Scenarios for var and :=

Because of the brevity and flexibility of short variable declarations, they are used to declare and initialize the majority of local variables. A var declaration tends to be reserved for local variables that need an explicit type that differs from that of the initializer expression, or for when the variable will be assigned a value letter and its initial value is unimportant.

A short variable declaration must declare at least one new variable.

Lifetime of variables

Golang maintains one graph for all the variables. The nodes in graph are the package or function entry, which are kind of like the namespace of the variable. When the variable is not reachable, it will be released.

How does the garbage collector know that a variable’s storage can be reclaimed? The full story is much more detailed than we need here, but the basic idea is that every package-level variable, and every local variable of each currently active function, can potentially be the start or root of a path to the variable in question, following pointers and other kinds of references that ultimately lead to the variable. If no such path exists, the variable has become unreachable, so it can no longer affect the rest of the computation.

Compiler will make the decision to assign the space of the variable to stack or heap, which could not be effected by user by method new.

A compiler may choose to allocate local variables on the heap or on the stack but, perhaps surprisingly, this choice is not determined by whether var or new was used to declare the variable.

Package Initialization

When one package is being intialized, the package-level variables are initialized in the order in which they are declared, except that dependencies are resolved first; Once when that has been finished and if the package has multiple .go files, they are initialized in the order of in which the files are given to the compiler; the go tool sorts .go files by name before invoking the compiler.

For some variables, we may need complex computation to initialize them. In that case, the init function mechanism may be simpler than directly assignment in package-level. Any file may contain any number of functions whose declaration is just func init() {/* ... */}. Whthin each file, init functions are automatically executed when the program starts, in the order in which they are declared.

Scope

Don’t confuse scope with lifetime. The scope of a declaration is a region of the program text; it is a compile-time property. The lifetime of a variable is the range of time during execution when the variable can be referred to by other parts of the program; it is a run-time property.

When the compiler encounters a reference to a name, it looks for a declaration, starting with the innermost enclosing lexical block and working up to the universe block. If the compiler finds no declaration, it reports an ‘‘undeclared name’’ error. If a name is declared in both an outer block and an inner block, the inner declaration will be found first. In that case, the inner declaration is said to shadow or hide the outer one, making it inaccessible

Given the concepts above, one kind of bug will be very fetal:

1
2
3
4
5
6
7
8
var cwd string
func init() {
cwd, err := os.Getwd() // NOTE: wrong!
if err != nil {
log.Fatalf("os.Getwd failed: %v", err)
}
log.Printf("Working directory = %s", cwd)
}

The global cwd variable remains uninitialized, and the apparently normal log output obfuscates the bug. Thus, we should be careful when using the global variable, each time we should try out best to use the assignment rather than short declaration syntax:

1
2
3
4
5
6
7
8
var cwd string
func init() {
var err error
cwd, err = os.Getwd()
if err != nil {
log.Fatalf("os.Getwd failed: %v", err)
}
}

Chapter 4 Composite Types

Arrays

The size of an array is part of its type, so [3]int and [4]int are different types. The size must be a constant expression, that is, an expression whose value can be computed as the program is being compiled.

When a function is called, a copy of eac hargument value is assigned to the corresponding parameter variable. In this regard, it would be inefficient to pass in the copied version. Using a pointer to an array is efficient and allows the called function to mutate the caller’s variable, but arrays are still inherently inflexible because of their fixed size. For these reasons, other than the specific situation, we will use slices for this purpose.

Slices

Slices represent variable-length sequences whose elements all have the same type. A slice type is written as []T, where the elements have type T. A slice has three components: a pointer, a length, and a capacity.

The slice operation s[i:j] would create a new slice that refers to elements in the range [i, j). Here s may be an array variable, a pointer to an array, or another slice. Note that this does not apply to the string. The slices on string would result in another string, rather than slices.

Also note that the length and capacity are two different concepts. For example, we can get a slice with size 3 from one array with size 10. But the capacity of the slice could not be 3. It could be 5. In this case, we can still get the access to the elements beyond the length. The slice would automatically extend itself.

When calling append() function, it is uncertain and not suggested for user to guess when will the re-allocation happen. The best way to use append is rune = append(rune, r). Updating the slice variable is required not just when calling append, but for any function that may change the length or capacity of a slice or make it refer to a different underlying array.

Map

The order of map iteration is unspecified, and different implementations might use a different hash function, leading to a different ordering. To enumerate the key/value pairs in order, we must sort the keys explicitly. Here is one comman pattern.

1
2
3
4
5
6
7
8
9
10
11
12
import "sort"

ages := make(map[string]int)
/* ... */
var names []string
for name := range ages {
names := append(names, name)
}
sort.Strings(names)
for _, name := range names {
fmt.Printf("%s\t%d\n", name, ages[name])
}

Since we know the final size of names from the outset, it is more efficient to allocate an array ofthe required size up front.

As with slices, maps cannot be compared to ech other; the only legal comparison is with nil. To test whether two maps contain the same keys and the same associated values, we must write a loop:

1
2
3
4
5
6
7
8
9
10
11
func equal(x, y map[string]int) bool {
if len(x) != len(y) {
return false
}
for k, xv := range x {
if yv, ok := y[k]; !ok || yv != xv {
return false
}
}
return true
}

Chapter 5. Functions

Anonymous Functions - Capturing Iteration Variables

Consider a program that must create a set of directories and later remove them. We can use a slice of function values to hold the clean-up operations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var rmdirs []func()
for _, d := range tempDirs() {
dir := d
os.MkdirAll(dir, 0755)
rmdirs = append(rmdirs, func() {
os.RemoveAll(dir)
})
}

// do some work

for _, rmdir := range rmdirs {
rmdir()
}

Here it is neccessary to assign the enumerating variable d to the local varibale dir. Because the lexical scope of d is within the for-loop. And the content of the variable will be changing during every iteration. Thus d holds the value from the final iteration, and consequently all calls to os.RemoveAll will attampt to remove the same directory if we don’t declare the local variable.

Chapter 6. Methods

Methods with a Pointer Receiver

Named types and pointers are the only types that may appear in a receiver delcaration. If the receiver is a variable of the struct but the method requires a pointer receiver, we can directly call the method by the variable. The compiler will perform an implicit &p on the variable. This works only for variables, including struct fields like p.X and array or slice elements like perim[0].

Composing Types by Struct Embedding

If we embed one struct into another struct without name, the embedding lets us take a syntactic shortcut to defining a struct that contains all the fields of embedded struct, plus some more. We can also call methods of the embedded field, even though the embedding struct has no declared methods.

1
2
3
4
5
6
type Point struct{ X, Y float64 }

type ColoredPoint struct {
Point
Color color.RGBA
}
1
2
3
4
5
6
7
8
9
red := color.RGBA{255, 0, 0, 255}

blue := color.RGBA{0, 0, 255, 255}

var p = ColoredPoint{Point{1, 1}, red}

var q = ColoredPoint{Point{5, 4}, blue}

fmt.Println(p.Distance(q.Point)) // "5" p.ScaleBy(2) q.ScaleBy(2) fmt.Println(p.Distance(q.Point)) // "10"

It may be tempted to view the embedded strcut as the base class and the embedding struct as a derived class. Note that we still have to give the embedded field in the parameter.

When the compiler resolves a selector such as p.ScaleBy to a method, it first looks for a directly declared method named ScaleBy, then for methods promoted once from ColoredPoint’s embedded fields, then for methods promoted twice from embedded fields within Point and RGBA, and so on. The compiler reports an error if the selector was ambiguous because two methods were promoted from the same rank.

Here is one nice trick to illustrate for struct embedding. The example shows part of a simple cache implemented using two package level variables.

1
2
3
4
5
6
7
8
9
10
11
var (
mu sync.Mutex // guards mapping
mapping = make(map[string]string)
)

func Lookup(key string) string {
mu.Lock()
v := mapping[key]
mu.Unlock()
return v
}

The version below is functionally equivalent but groups together the two related variables in a single package-level variable, cache:

1
2
3
4
5
6
7
8
9
10
11
12
13
var cache = struct {
sync.Mutex
mapping map[string]string
} {
mapping: make(map[string]string),
}

func Lookup(key string) string {
cache.Lock()
v := cache.mapping[key]
cache.Unlock()
return v
}

Encapsulation

Encapsulation provides three benefits.

  • clients cannot directly modify the object’s variables, one need inspect fewer statements to understand the possible vaules of those variables.
  • hiding emplementation details prevents clients from depending on things that mighht change, which gives designer greater freedom to evolve the implementation without breaking API compatibility.
  • it prevents clients from setting an object’s variables arbitrarily.

Chapter 7.Interfaces

Interface Satisfaction

It is kind of subtle in what it means for a type to have a method. Recall that for each named concrete type T, some of its methods have a receiver of type T itself, whereas others require a T pointer. Recall also that it is legal to call a T method on an argument of type T so long as the argument is a variable: the compiler implicitly takes its address. But this is mere syntactic sugar.

1
2
3
var s IntSet
var _ fmt.Stringer = &s // OK
var _ fmt.Stringer = s // compile error: IntSet lacks String method

Like an envelope that wraps and conceals the letter it holds, an interface wraps and conceals the concrete type and value that it holds. Only the methods revealed by the interface type may be called, even if the concrete type has others.

Interface Value

Interface is abstract type, which is different from the concrete type such as int, string, etc. And when the variable is declared as the interface, its type and value are both nil. Only after we assign it to one new variable which is satisfied with the pre-defined interface, its type and value change.

Interface values may be compared using == and !=. Two interface values are equal if both are nil, or if their dynamic types are identical and their dynamic values are equal according to the usual behavior of == for that type.

Caveat: An Interface Containing a Nil Pointer Is Non-Nil.

A Few Words of Advice

When designing a new package, novice Go programmers often start by creating a set of interfaces and only later define the concrete types that satisfy them. This approach results in many interfaces, each of which has only a single implementation. Don’t do that. Such interfaces are unnecessary abstractions; they also have a run-time cost. You can restrict which methods of a type or fields of a struct are visible outside a package using the export mechanism (§6.6). Interfaces are only needed when there are two or more concrete types that must be dealt with in a uniform way.

We make an exception to this rule when an interface is satisfied by a single concrete type but that type cannot live in the same package as the interface because of its dependencies. In that case, an interface is a good way to decouple two packages.

0%