Golang Learning Notes
- 1. Foramtting
- 2. Commentary
- 3. Names
- 4. Semicolons
- 5. Control structures
- 6. Data types
- 7. Initialization
- 8. Concurrency
- 9. Errors
- 10. Testing
- 11. Modules
- 12. Printf
- References
1. Foramtting
With Go we take an unusual approach and let the machine take care of most formatting issues. The gofmt
program (also available as go fmt
, which operates at the package level rather than source file level) reads a Go program and emits the source in a standard style of indentation and vertical alignment, retaining and if necessary reformatting comments.
$ cat formatting.go && gofmt formatting.go
package formatting
type T struct {
name string // name of the object
value int // its value
}
package formatting
type T struct {
name string // name of the object
value int // its value
}
2. Commentary
Go provides C-style /* */
block comments and C++-style //
line comments. Line comments are the norm; block comments appear mostly as package comments, but are useful within an expression or to disable large swaths of code.
The program—and web server--godoc
processes Go source files to extract documentation about the contents of the package. Comments that appear before top-level declarations, with no intervening newlines, are extracted along with the declaration to serve as explanatory text for the item. The nature and style of these comments determines the quality of the documentation godoc produces.
/*
Package regexp implements a simple library for regular expressions.
The syntax of the regular expressions accepted is:
regexp:
concatenation { '|' concatenation }
concatenation:
{ closure }
closure:
term [ '*' | '+' | '?' ]
term:
'^'
'$'
'.'
character
'[' [ '^' ] character-ranges ']'
'(' regexp ')'
*/
package regexp
$ go doc builtin new
package builtin // import "builtin"
func new(Type) *Type
The new built-in function allocates memory. The first argument is a type,
not a value, and the value returned is a pointer to a newly allocated zero
value of that type.
$ go doc sync Mutex
package sync // import "sync"
type Mutex struct {
// Has unexported fields.
}
A Mutex is a mutual exclusion lock. The zero value for a Mutex is an
unlocked mutex.
A Mutex must not be copied after first use.
func (m *Mutex) Lock()
func (m *Mutex) Unlock()
3. Names
The visibility of a name outside a package is determined by whether its first character is upper case.
3.1. Package names
-
By convention, packages are given lower case, single-word names; there should be no need for underscores or mixedCaps.
-
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 namebase64
, notencoding_base64
and notencodingBase64
. -
Use the package structure to help you choose good names.
-
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 stutter.
For instance, the buffered reader type in the
bufio
package is calledReader
, notBufReader
, because users see it asbufio.Reader
, which is a clear, concise name. -
Moreover, because imported entities are always addressed with their package name,
bufio.Reader
does not conflict withio.Reader
. -
Similarly, the function to make new instances of
ring.Ring
—which is the definition of a constructor in Go—would normally be calledNewRing
, but sinceRing
is the only type exported by the package, and since the package is calledring
, it’s called justNew
, which clients of the package see asring.New
.
-
3.2. Getters
-
Go doesn’t provide automatic support for getters and setters.
-
There’s nothing wrong with providing getters and setters yourself, and it’s often appropriate to do so, but it’s neither idiomatic nor necessary to put
Get
into the getter’s name. -
If you have a field called
owner
(lower case, unexported), the getter method should be calledOwner
(upper case, exported), notGetOwner
. -
A setter function, if needed, will likely be called
SetOwner
. -
Both names read well in practice:
owner := obj.Owner() if owner != user { obj.SetOwner(user) }
3.3. 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. -
There are a number of such names and it’s productive to honor them and the function names they capture.
-
Read
,Write
,Close
,Flush
,String
and so on have canonical signatures and meanings. -
To avoid confusion, don’t give your method one of those names unless it has the same signature and meaning.
-
Conversely, if your type implements a method with the same meaning as a method on a well-known type, give it the same name and signature; call your string-converter method
String
notToString
.
3.4. MixedCaps
Finally, the convention in Go is to use MixedCaps
or mixedCaps
rather than underscores to write multiword names.
4. Semicolons
-
Like C, Go’s formal grammar uses semicolons to terminate statements, but unlike in C, those semicolons do not appear in the source.
If the newline comes after a token that could end a statement, insert a semicolon.
-
Idiomatic Go programs have semicolons only in places such as for loop clauses, to separate the initializer, condition, and continuation elements.
-
They are also necessary to separate multiple statements on a line, should you write code that way.
5. Control structures
-
There is no
do
orwhile
loop, only a slightly generalizedfor
;switch
is more flexible; -
if
andswitch
accept an optional initialization statement like that offor
; -
break
andcontinue
statements take an optional label to identify what to break or continue; -
and there are new control structures including a type switch and a multiway communications multiplexer,
select
. -
There are no parentheses and the bodies must always be brace-delimited.
5.1. If
if x > 0 {
return y
}
if f, err: = os.Open(name); err != nil {
return err
}
5.2. For
// Like a C for
for init; condition; post { }
// Like a C while
for condition { }
// Like a C for(;;)
for { }
// Like a C do-while
for {
// do something
if condition; {
break
}
}
If you’re looping over an array, slice, string, or map, or reading from a channel, a range
clause can manage the loop.
for key, value := range map {
}
// If you only need the second item in the range (the value),
// use the blank identifier, an underscore, to discard the first:
for _, value := range map {
}
for index, value := range array {
}
for value := range channel {
}
For strings, the range
does more work for you, breaking out individual Unicode code points by parsing the UTF-8. Erroneous encodings consume one byte and produce the replacement rune U+FFFD. (The name (with associated builtin type) rune
is Go terminology for a single Unicode code point.)
for pos, char := range "日本\x80語" { // \x80 is an illegal UTF-8 encoding
fmt.Printf("character %#U starts at byte position %d\n", char, pos)
}
// Output:
// 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
Go has no comma operator and ++
and --
are statements not expressions. Thus if you want to run multiple variables in a for you should use parallel assignment (although that precludes ++ and --).
// 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]
}
5.3. Switch
Go’s switch is more general than C’s.
-
The expressions need not be constants or even integers,
-
the cases are evaluated top to bottom until a match is found,
-
and if the
switch
has no expression it switches ontrue
. -
It’s therefore possible—and idiomatic—to write an
if-else-if-else
chain as aswitch
. -
There is no automatic fall through, but cases can be presented in comma-separated lists.
-
Although they are not nearly as common in Go as some other C-like languages,
break
statements can be used to terminate aswitch
early. -
Sometimes, though, it’s necessary to break out of a surrounding loop, not the switch, and in Go that can be accomplished by putting a label on the loop and "breaking" to that label.
-
Of course, the
continue
statement also accepts an optional label but it applies only to loops.
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}
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]<<shift)
}
}
5.3.1. Type switch
A switch can also be used to discover the dynamic type of an interface variable.
-
Such a type switch uses the syntax of a type assertion with the keyword
type
inside the parentheses. -
If the switch declares a variable in the expression, the variable will have the corresponding type in each clause.
-
It’s also idiomatic to reuse the name in such cases, in effect declaring 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
}
5.4. Select
The select
statement lets a goroutine wait on multiple communication operations.
A select
blocks until one of its cases can run, then it executes that case. It chooses one at random if multiple are ready.
The default
case in a select
is run if no other case is ready.
Use a default
case to try a send or receive without blocking:
func main() {
tick := time.Tick(100 * time.Millisecond)
boom := time.After(500 * time.Millisecond)
for {
select {
case <-tick:
fmt.Println("tick.")
case <-boom:
fmt.Println("BOOM!")
return
default:
fmt.Println(" .")
time.Sleep(50 * time.Millisecond)
}
}
}
5.5. Break, Continue and Goto
A break
statement terminates execution of the innermost for
, switch
, or select
statement within the same function.
A continue
statement begins the next iteration of the innermost for
loop at its post statement within the same function.
A goto
statement transfers control to the statement with the corresponding label within the same function.
func main() {
tick := time.Tick(100 * time.Millisecond)
boom := time.After(500 * time.Millisecond)
RawLoop:
for {
select {
case <-tick:
fmt.Println("tick.")
continue RawLoop // just for demo, needless
case <-boom:
fmt.Println("BOOM!")
break RawLoop
default:
fmt.Println(". .")
time.Sleep(50 * time.Millisecond)
goto RawLoop // just for demo, needless
}
}
}
5.6. Fallthrough
A fallthrough
statement transfers control to the first statement of the next case clause in an expression switch
statement. It may be used only as the final non-empty statement in such a clause.
switch {
case 10 > 11:
fmt.Println("10 > 11")
case 1 < 5:
fmt.Println("1 < 5")
fallthrough
case 1 > 10:
fmt.Println("1 > 10 ?")
}
// Output:
// 1 < 5
// 1 > 10 ?
5.7. Defer
A defer
statement invokes a function whose execution is deferred to the moment the surrounding function returns,
-
either because the surrounding function executed a return statement, reached the end of its function body,
godir, err := os.Open("/usr/local/go") if err != nil { log.Printf("%s\n", err) defer godir.Close() }
-
or because the corresponding goroutine is panicking.
defer func() { e := recover() fmt.Printf("recover: %s\n", e) }() defer func() { fmt.Println(". . .") }() panic(fmt.Sprintf("Oops, I'm NOT myself.")) // Output: // . . . // recover: Oops, I'm NOT myself.
-
Go’s
defer
statement schedules a function call (the deferred function) to be run immediately before the function executing thedefer
returns.// The arguments to the deferred function (which include the receiver if the function is a method) // are evaluated when the _defer_ executes, not when the _call_ executes. func main() { v := 10 defer fmt.Println(3 * v) // 30 defer func() { fmt.Println(v) // 20 }() defer func(x int) { fmt.Println(x) // 10 }(v) v = 20 _ = v } // Output: // 10 // 20 // 30
-
It’s an unusual but effective way to deal with situations such as resources that must be released regardless of which path a function takes to return.
func ReadFile(filename string) ([]byte, error) { f, err := os.Open(filename) if err != nil { return nil, err } defer f.Close() return ReadAll(f) }
-
Deferred functions are executed in LIFO order (stacking style).
for i := 0; i < 5; i++ { defer fmt.Printf("%d ", i) } // Output: // 4 3 2 1 0
// All function values created by this loop "capture" // and share the same variable—an addressable storage location, // not its value at that particular moment. for i := 0; i < 5; i++ { defer func() { fmt.Print(i, " ") }() } // Output: // 5 5 5 5 5
for i := 0; i < 5; i++ { // declares inner i, intialized to outer i i := i defer func() { fmt.Print(i, " ") }() } // Output: // 4 3 2 1 0
6. Data types
// any is an alias for interface{} and is equivalent to interface{} in all ways.
// (go1.18)
any
// comparable is an interface that is implemented by all comparable types
// (booleans, numbers, strings, pointers, channels, arrays of comparable types,
// structs whose fields are all comparable types). The comparable interface may
// only be used as a type parameter constraint, not as the type of a variable.
// (go1.18)
comparable
bool // true false
string
int8 int16 int32 int64
uint8 uint16 uint32 uint64 uintptr
int uint // either 32 or 64 bits
byte // alias for uint8
rune // alias for int32
// represents a Unicode code point
float32 float64
complex64 complex128
// more types
pointers structs array slices maps functions interfaces channels
6.1. Strings, bytes, runes and characters
-
Go source code is always UTF-8.
-
A string holds arbitrary bytes.
-
A string literal, absent byte-level escapes, always holds valid UTF-8 sequences.
-
Those sequences represent Unicode code points, called runes.
-
No guarantee is made in Go that characters in strings are normalized.
const nihongo = "日本語"
for _, runeValue := range nihongo {
fmt.Printf("%#U ", runeValue)
}
fmt.Println()
// U+65E5 '日' U+672C '本' U+8A9E '語'
for index := 0; index < len(nihongo); index++ {
fmt.Printf("%x ", nihongo[index])
}
fmt.Println()
// e6 97 a5 e6 9c ac e8 aa 9e
for index := 0; index < len(nihongo); index++ {
fmt.Printf("%q ", nihongo[index])
}
fmt.Println()
// 'æ' '\u0097' '¥' 'æ' '\u009c' '¬' 'è' 'ª' '\u009e'
for index := 0; index < len(nihongo); index++ {
fmt.Printf("%+q ", nihongo[index])
}
fmt.Println()
// '\u00e6' '\u0097' '\u00a5' '\u00e6' '\u009c' '\u00ac' '\u00e8' '\u00aa' '\u009e'
6.2. Pointers
// A pointer holds the memory address of a value.
// Unlike C, Go has no pointer arithmetic.
// The type `*T` is a pointer to a `T` value. Its zero value is `nil`.
var p *int
i := 42
// The `&` operator generates a pointer to its operand.
p = &i
// The `*` operator ("dereferencing" or "indirecting") denotes the pointer's underlying value.
*p = 21
6.3. Structs
// A struct is a collection of fields.
type Vertex struct {
X, Y int
}
var (
// A struct literal denotes a newly allocated struct value by listing the values of its fields.
v1 = Vertex{1, 2} // has type Vertex
// You can list just a subset of fields by using the Name: syntax.
// (And the order of named fields is irrelevant.)
v2 = Vertex{X: 1} // Y:0 is implicit
v3 = Vertex{} // X:0 and Y:0
// The special prefix & returns a pointer to the struct value
p = &Vertex{1, 2} // has type *Vertex
)
func main() {
// Struct fields are accessed using a dot.
p.X = 1e9
fmt.Println(v1, p, v2, v3)
}
6.4. Arrays
-
The type
[n]T
is an array ofn
values of typeT
. -
Arrays are values. Assigning one array to another copies all the elements.
In particular, if you pass an array to a function, it will receive a copy of the array, not a pointer to it.
-
The size of an array is part of its type.
The types
[10]int
and[20]int
are distinct, so arrays cannot be resized.
var a [2]string
a[0] = "Hello"
a[1] = "World"
// an array literal
primes := [6]int{2, 3, 5, 7, 11, 13}
6.5. Slices
-
A slice, on the other hand, is a dynamically-sized, flexible view into the elements of an array.
-
The type
[]T
is a slice with elements of typeT
. -
A slice is formed by specifying two indices, a low and high bound, separated by a colon:
// This selects a half-open range which includes the first element, but excludes the last one. a[low : high]
-
The following expression creates a slice which includes elements 1 through 3 of
a
:a[1:4]
Slices are like references to arrays
-
A slice does not store any data, it just describes a section of an underlying array.
-
A slice hold references to an underlying array, and if you assign one slice to another, both refer to the same array.
-
Changing the elements of a slice modifies the corresponding elements of its underlying array.
-
Other slices that share the same underlying array will see those changes.
Slice literals
-
A slice literal is like an array literal without the length.
[]bool{true, true, false}
Slice defaults
-
When slicing, you may omit the high or low bounds to use their defaults instead.
-
The default is zero for the low bound and the length of the slice for the high bound.
// For the array var a [10]int // these slice expressions are equivalent: a[0:10] a[:10] a[0:] a[:]
Slice length and capacity
-
A slice has both a length and a capacity.
-
The length of a slice is the number of elements it contains.
-
The capacity of a slice is the number of elements in the underlying array, counting from the first element in the slice.
-
The length and capacity of a slice
s
can be obtained using the expressionslen(s)
andcap(s)
. -
You can extend a slice’s length by re-slicing it, provided it has sufficient capacity.
Nil slices
-
The zero value of a slice is
nil
. -
A
nil
slice has a length and capacity of 0 and has no underlying array.
Appending to a slice
-
It is common to append new elements to a slice, and so Go provides a built-in
append
function.func append(s []T, vs ...T) []T
-
The resulting value of
append
is a slice containing all the elements of the original slice plus the provided values. -
If the backing array of
s
is too small to fit all the given values a bigger array will be allocated. The returned slice will point to the newly allocated array.var s []int // append works on nil slices. s = append(s, 0) // The slice grows as needed. s = append(s, 1) // We can add more than one element at a time. s = append(s, 2, 3, 4)
6.6. Maps
-
Maps are a convenient and powerful built-in data structure that associate values of one type (the key) with values of another type (the element or value).
-
The key can be of any type that is comparable for which the equality operator is defined.
The language spec defines the Comparison operators precisely, in short, comparable types are boolean, numeric, string, pointer, channel, and interface types, and structs or arrays that contain only those types.[BLOGMAPS]
Notably absent from the list are slices, maps, and functions; these types cannot be compared using
==
, and may not be used as map keys. -
Slices cannot be used as map keys, because equality is not defined on them.
-
Like slices, maps hold references to an underlying data structure.
If you pass a map to a function that changes the contents of the map, the changes will be visible in the caller.
-
The zero value of a map is
nil
.A
nil
map has no keys, nor can keys be added. -
Map literals are like struct literals, but the keys are required.
var m map[string]int // <nil> m = map[string]int{ "hello": 100, "world": 200, }
-
The
make
function returns a map of the given type with an optional capacity hint as arguments, initialized and ready for use.// m := make(map[string]int, 100) m := make(map[string]int) // insert or update an element m["Answer"] = 42 // delete an element: // The delete function doesn’t return anything, and will do nothing if the specified key doesn’t exist. delete(m, "Answer") // retrieve an element // If the requested key doesn’t exist, we get the value type’s zero value. v := m["Answer"] // test that a key is present with a two-value assignment v, ok := m["Answer"]
6.7. Functions
-
A function can return any number of results.
func (file *File) Write(b []byte) (n int, err error)
-
The return or result "parameters" of a Go function can be given names and used as regular variables, just like the incoming parameters.
When named, they are initialized to the zero values for their types when the function begins; if the function executes a
return
statement with no arguments, the current values of the result parameters are used as the returned values. -
Functions are values too. They can be passed around just like other values. Function values may be used as function arguments and return values.
-
Go functions may be closures, that is a function value that references (i.e. bounds to) variables from outside its body.
func adder() func(int) int { sum := 0 return func(x int) int { sum += x return sum } } func w(s func(int) int, i int) int { return s(i) } func main() { pos, neg := adder(), adder() for i := 1; i <= 3; i++ { fmt.Printf("%+d, %+2d\n", w(pos, i), neg(-i)) } } // Output: // +1, -1 // +3, -3 // +6, -6
-
A function literal represents an anonymous function and cannot declare type parameters, and it can be assigned to a variable or invoked directly.
-
Function literals are closures: they may refer to variables defined in a surrounding function, which are then shared between the surrounding function and the function literal, and they survive as long as they are accessible.
func main() { var sum int var add = func() { sum += 1 } add() fmt.Printf("%d\n", sum) } // Output: // 1
6.8. Methods
A method is a function with a special receiver argument, defined on any named type (except a pointer or an interface) in the same package.
type MyInt int
func (mi *MyInt) String() string {
return fmt.Sprintf("Hi %d!", *mi)
}
func (mi *MyInt) Add(delta int) {
*mi = *mi + MyInt(delta)
}
func main() {
var mi MyInt = 1_024
mi.Add(1_024)
fmt.Println(&mi)
}
// Output:
// Hi 2048!
Choosing a value or pointer receiver
-
There are two reasons to use a pointer receiver.
-
The first is so that the method can modify the value that its receiver points to.
-
The second is to avoid copying the value on each method call, that can be more efficient if the receiver is a large struct, for example.
-
-
In general, all methods on a given type should have either value or pointer receivers, but not a mixture of both.
-
The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers.
When the value is addressable, the language takes care of the common case of invoking a pointer method on a value by inserting the address operator ( &
) automatically.
Nil is a valid receiver value
-
Just as some functions allow nil pointers as arguments, so do some methods for their receiver, especially if
nil
is a meaningful zero value of the type, as with maps and slices. -
When you define a type whose methods allow
nil
as a receiver value, it’s worth pointing this out explicitly in its documentation comment.package bytes // import "bytes" type Buffer struct { // Has unexported fields. } A Buffer is a variable-sized buffer of bytes with Read and Write methods. The zero value for Buffer is an empty buffer ready to use.
6.9. Interfaces
An interface type defines a type set.
-
A variable of interface type can store a value of any type that is in the type set of the interface. Such a type is said to implement the interface.
-
The value of an uninitialized variable of interface type is
nil
. -
An interface type is specified by a list of interface elements.
An interface element is either a method or a type element, where a type element is a union of one or more type terms. A type term is either a single type or a single underlying type.
Interfaces in Go provide a way to specify the behavior of an object: if something can do this, then it can be used here.
Interfaces are implemented implicitly
-
A type implements an interface by implementing its methods. There is no explicit declaration of intent, no "implements" keyword.
-
Implicit interfaces decouple the definition of an interface from its implementation, which could then appear in any package without prearrangement.
Interface values
-
Under the hood, interface values can be thought of as a tuple of a value and a concrete type:
An interface value holds a value of a specific underlying concrete type.
(value, type)
-
Calling a method on an interface value executes the method of the same name on its underlying type.
Interface values with nil underlying values
-
If the concrete value inside the interface itself is nil, the method will be called with a nil receiver.
-
In some languages this would trigger a null pointer exception, but in Go it is common to write methods that gracefully handle being called with a nil receiver.
-
Note that an interface value that holds a nil concrete value is itself non-nil.
type I interface { M() } type T struct{} func (t *T) M() { if t == nil { fmt.Println("<nil receiver>") return } } func main() { var i I // `i` is nil // i.M() // runtime error: invalid memory address or nil pointer dereference var t *T i = t // `i` is not nil, but the concrete type `t` is nil i.M() fmt.Printf("%v, %T\n", i, i) i = &T{} // the concrete type `t` is not nil i.M() fmt.Printf("%v, %T\n", i, i) } // Output: // <nil receiver> // <nil>, *main.T // &{}, *main.T
Nil interface values
-
A nil interface value holds neither value nor concrete type.
-
Calling a method on a nil interface is a run-time error because there is no type inside the interface tuple to indicate which concrete method to call.
var i I fmt.Printf("(%v, %T)\n", i, i) i.M() // (<nil>, <nil>) // panic: runtime error: invalid memory address or nil pointer dereference
The empty interface
-
The interface type that specifies zero methods is known as the empty interface:
interface{}
-
An empty interface may hold values of any type. (Every type implements at least zero methods.)
-
Empty interfaces are used by code that handles values of unknown type.
-
For convenience, the predeclared type
any
is an alias for the empty interface.$ go doc builtin.any package builtin // import "builtin" type any = interface{} any is an alias for interface{} and is equivalent to interface{} in all ways.
Basic interfaces
-
Interfaces whose type sets can be defined entirely by a list of methods are called basic interfaces.
// A simple File interface. interface { Read([]byte) (int, error) Write([]byte) (int, error) Close() error }
Embedded interfaces
-
In a slightly more general form an interface T may use a (possibly qualified) interface type name E as an interface element, which is called embedding interface E in T.
-
The type set of T is the intersection of the type sets defined by T’s explicitly declared methods and the type sets of T’s embedded interfaces.
In other words, the type set of T is the set of all types that implement all the explicitly declared methods of T and also all the methods of E.
type Reader interface { Read(p []byte) (n int, err error) Close() error } type Writer interface { Write(p []byte) (n int, err error) Close() error } // ReadWriter's methods are Read, Write, and Close. type ReadWriter interface { Reader // includes methods of Reader in ReadWriter's method set Writer // includes methods of Writer in ReadWriter's method set }
-
When embedding interfaces, methods with the same names must have identical signatures.
type ReadCloser interface { Reader // includes methods of Reader in ReadCloser's method set Close() // illegal: signatures of Reader.Close and Close are different }
General interfaces
Azure AI | ChatGPT 4
In Go, general interfaces are used to define type constraints for type parameters in generic functions and data structures. [ChatGpt4]
// Comparable is an interface that defines a type constraint using // a union of types (int, float64, and string). This means that a // type parameter satisfying the Comparable constraint must be one // of these types. type Comparable interface { int | float64 | string } // The Max function is defined as a generic function using the type // parameter T. The type parameter is specified within square brackets // ([T Comparable]). It means that the function can work with any type // T that satisfies the Comparable constraint. func Max[T Comparable](a, b T) T { if a > b { return a } return b } // The Max function is called with different types of arguments (int, // float64, and string). The type parameter T is replaced with the actual // type of the arguments at each call, allowing the function to work with // different types while maintaining type safety. func main() { fmt.Println(Max(3, 4)) // int fmt.Println(Max(2.5, 3.7)) // float64 fmt.Println(Max("apple", "cat")) // string }
In summary, while you can’t assign a concrete type to a general interface, general interfaces are used to specify what types can be used with a generic function or data structure.
These type constraints allow you to create flexible and reusable generic code while maintaining type safety.
-
In their most general form, an interface element may also be an arbitrary type term
T
, or a term of the form~T
specifying the underlying typeT
, or a union of termst1|t2|…|tn
. -
By construction, an interface’s type set never contains an interface type.
// An interface representing only the type int. interface { int } // An interface representing all types with underlying type int. interface { ~int } // An interface representing all types with underlying type int that implement the String method. interface { ~int String() string } // An interface representing an empty type set: there is no type that is both an int and a string. // // This code defines an interface that no concrete type satisfies because there is no type that is // both an int and a string. It is not the same as an empty interface (interface{}), which any type // can satisfy. This code snippet is used to illustrate the concept of an unsatisfiable interface // in the Go language specification. (Azure AI | ChatGPT 4) // // While this interface can be compiled, it cannot be used in practical terms because no type can // satisfy the constraints. It's a theoretical construct to show the capabilities and limitations of // the type constraint system in Go. (Azure AI | ChatGPT 4) interface { int string }
-
In a term of the form
~T
, the underlying type ofT
must be itself, andT
cannot be an interface.Azure AI | ChatGPT 4
The tilde symbol
~
defines a type set constraint that includes the underlying type of T as well as any other types whose underlying type is the same as T.In this context, "underlying type" refers to the actual type without any type aliases or defined types based on it.
-
"The underlying type of T must be itself": This means that when defining a type set constraint using
~T
, the type T must not be a type alias or a defined type based on another type. Instead, T must be a "base" type, such as int, float64, or a struct type. -
"T cannot be an interface": This condition states that the type T in a type set constraint using ~T should not be an interface type. This is because interface types don’t have a specific underlying type that can be used in the type set definition.
type MyInt int type MyI interface { ~[]byte // the underlying type of []byte is itself ~MyInt // illegal: the underlying type of MyInt is not MyInt ~error // illegal: error is an interface }
-
-
Union elements denote unions of type sets:
// The Float interface represents all floating-point types // (including any named types whose underlying types are // either float32 or float64). type Float interface { ~float32 | ~float64 }
Generality
-
If a type exists only to implement an interface and will never have exported methods beyond that interface, there is no need to export the type itself.
-
Exporting just the interface makes it clear the value has no interesting behavior beyond what is described in the interface.
-
It also avoids the need to repeat the documentation on every instance of a common method.
-
In such cases, the constructor should return an interface value rather than the implementing type.
Interface conversions and type assertions
-
A type assertion provides access to an interface value’s underlying concrete value.
t := i.(T)
This statement asserts that the interface value
i
holds the concrete typeT
and assigns the underlyingT
value to the variablet
.If
i
does not hold aT
, the statement will trigger a panic. -
To test whether an interface value holds a specific type, a type assertion can return two values: the underlying value and a boolean value that reports whether the assertion succeeded.
t, ok := i.(T)
If
i
holds aT
, thent
will be the underlying value andok
will betrue
.If not,
ok
will befalse
andt
will be the zero value of typeT
, and no panic occurs.
Type switches
-
The declaration in a type switch has the same syntax as a type assertion
i.(T)
, but the specific typeT
is replaced with the keywordtype
.switch v := i.(type) { case T: // here v has type T case S: // here v has type S default: // no match; here v has the same type as i }
6.10. Embedding: interfaces and structs
-
Go does not provide the typical, type-driven notion of subclassing, but it does have the ability to “borrow” pieces of an implementation by embedding types within a struct or interface.
package io // import "io" type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } // ReadWriter is the interface that combines the Reader and Writer interfaces. type ReadWriter interface { Reader Writer }
package bufio // import "bufio" type Reader struct { // Has unexported fields. } func (b *Reader) Read(p []byte) (n int, err error) type Writer struct { // Has unexported fields. } func (b *Writer) Write(p []byte) (nn int, err error) // ReadWriter stores pointers to a Reader and a Writer. // It implements io.ReadWriter. type ReadWriter struct { *Reader *Writer }
-
For the embedded type, the methods of that type become methods of the outer type, but when they are invoked the receiver of the method is the inner type, not the outer one.
For example, when the
Read
method of abufio.ReadWriter
is invoked, the receiver is theReader
field of theReadWriter
, not theReadWriter
itself.type Reader struct { } func (r *Reader) Read() { fmt.Println("Read") } type Writer struct { } func (r *Writer) Write() { fmt.Println("Write") } type ReadWriter struct { *Reader *Writer } func main() { rw := ReadWriter{} rw.Read() // same as rw.Reader.Read() // Output: // Read }
-
Embedding types introduces the problem of name conflicts but the rules to resolve them are simple.
-
First, a field or method
X
hides any other itemX
in a more deeply nested part of the type. -
Second, if the same name appears at the same nesting level, it is usually an error.
However, if the duplicate name is never mentioned in the program outside the type definition, it is OK.
-
Embedding:
-
Provides an "is-a" relationship where the outer struct or interface directly inherits the fields and methods of the embedded struct or interface.
-
Allows for easier and more direct access to the fields and methods of the embedded struct or interface, as they become part of the outer struct or interface.
-
Enhances code reuse and polymorphism by making the fields and methods of the embedded struct or interface available directly in the outer struct or interface.
Not Embedding (Composing):
-
Provides a "has-a" relationship where the outer struct or interface holds instances of other structs or interfaces as separate fields.
-
Requires explicitly accessing the fields and methods of the inner structs or interfaces through the composed fields.
-
Keeps a clear separation between the fields and methods of the outer struct or interface and the inner structs or interfaces it holds.
type Reader struct { } func (r *Reader) Read() { fmt.Printf("Read.\n") } type Writer struct { } func Write() { fmt.Printf("Write.\n") } type ReadWriter struct { reader *Reader writer *Writer } func main() { rw := &ReadWriter{&Reader{}, &Writer{}} rw.reader.Read() // Output: Read. rw.Read() // Compiler error: rw.Read undefined (type *ReadWriter has no field or method Read) }
6.11. Channels
-
Channels are a typed conduit through which you can send and receive values with the channel operator,
<-
.ch <- v // Send v to channel ch. v := <-ch // Receive from ch, and assign value to v. // (The data flows in the direction of the arrow.)
-
Like maps and slices, channels must be created before use:
// By default, sends and receives block until the other side is ready. // This allows goroutines to synchronize without explicit locks or condition variables. blockChan := make(chan int) // Sends to a buffered channel block only when the buffer is full. // Receives block when the buffer is empty. bufChan := make(chan int, 100)
-
A sender can
close
a channel to indicate that no more values will be sent.-
After calling close, and after any previously sent values have been received, receive operations will return the zero value for the channel’s type without blocking.
-
Note that it is only necessary to close a channel if the receiver is looking for a close. Closing the channel is a control signal on the channel indicating that no more data follows.
-
The multi-valued assignment form of the receive operator reports whether a received value was sent before the channel was closed.
// ok is false if there are no more values to receive and the channel is closed. v, ok := <-ch
-
The loop for
v := range c
receives values from the channel repeatedly until it is closed. -
Attempting to close an already-closed channel causes a panic, as does closing a nil channel.
-
Sending to a closed channel causes a run-time panic.
-
Note: Only the sender should close a channel, never the receiver.
Sending on a closed channel will cause a panic.
-
Another note: Channels aren’t like files; you don’t usually need to close them.
Closing is only necessary when the receiver must be told there are no more values coming, such as to terminate a
range
loop.
-
-
A channel may be constrained only to send or only to receive by assignment or explicit conversion.
func main() { var ( _ = make(chan int) // bidirectional _ = make(<-chan int) // receive-only _ = make(chan<- int) // send-only ) ch := make(chan int) // send-only go func(ch chan<- int) { for i := 0; i < 3; i++ { ch <- i } close(ch) }(ch) // receive-only go func(ch <-chan int) { for v := range ch { fmt.Println(v) } }(ch) time.Sleep(time.Millisecond) // Output: // 0 // 1 // 2 }
func main() { ch1 := make(chan int) ch2 := make(chan int, 2) // buffering channel quit := make(chan int) go func() { for i := 1; ; i++ { ch1 <- 2 * i time.Sleep(time.Duration(rand.Intn(1e3)) * time.Millisecond) } }() go func(ch chan<- int) { for i := 1; ; i++ { ch <- 2*i + 1 time.Sleep(time.Duration(rand.Intn(1e3)) * time.Millisecond) } }(ch2) go func() { <-time.After(time.Duration(5e3) * time.Millisecond) quit <- 0 }() // The select statement lets a goroutine wait on multiple communication operations. // A select blocks until one of its cases can run, then it executes that case. // It chooses one at random if multiple are ready. ch3 := make(chan int) timeout := time.After(500 * time.Millisecond) go func() { defer close(ch3) for { // multiplexing: ch1 + ch2 => ch3 select { case ch3 <- <-ch1: case ch3 <- <-ch2: case <-timeout: fmt.Println("You're too slow.") return case <-quit: fmt.Println("Quit.") return } } }() for v := range ch3 { fmt.Println(v) } }
6.12. Type conversions
The expression T(v)
converts the value v
to the type T
.
// Some numeric conversions:
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
// Or, put more simply:
i := 42
f := float64(i)
u := uint(f)
6.13. Generics
-
Go functions can be written to work on multiple types using type parameters.
The type parameters of a function appear between brackets, before the function’s arguments.
func Index[T comparable](s []T, x T) int
This declaration means that
s
is a slice of any typeT
that fulfills the built-in constraintcomparable
.x
is also a value of the same type.comparable
is a useful constraint that makes it possible to use the==
and!=
operators on values of the type.package builtin // import "builtin" type comparable interface{ comparable } comparable is an interface that is implemented by all comparable types (booleans, numbers, strings, pointers, channels, arrays of comparable types, structs whose fields are all comparable types). The comparable interface may only be used as a type parameter constraint, not as the type of a variable.
-
In addition to generic functions, Go also supports generic types.
A type can be parameterized with a type parameter, which could be useful for implementing generic data structures.
type ComparableOrdered interface { comparable constraints.Ordered // "golang.org/x/exp/constraints" } // List represents a singly-linked list that holds values of `ComparableOrdered` type. type List[T ComparableOrdered] struct { next *List[T] val T } func (head *List[T]) append(vals ...T) { var a = func(val T) { tail := head for tail.next != nil { tail = tail.next } tail.next = &List[T]{val: val} } for _, val := range vals { a(val) } } func (head *List[T]) max() T { max := head.val node := head.next for node != nil { if node.val > max { max = node.val } node = node.next } return max } func (head *List[T]) String() string { var b strings.Builder node := head for node != nil { fmt.Fprintf(&b, "%v", node.val) node = node.next if node != nil { fmt.Fprint(&b, " -> ") } } return b.String() } func main() { list := &List[int]{val: 20} list.append(10, 30, 60) list.append(40) fmt.Printf("list: %v\n", list) fmt.Printf("max: %v", list.max()) // Output: // list: 20 -> 60 -> 30 -> 10 -> 40 // max: 60 }
-
Type constraint and type parameter
-
A type constraint is an interface that defines the set of permissible type arguments for the respective type parameter and controls the operations supported by values of that type parameter.
-
If the constraint is an interface literal of the form
interface{E}
whereE
is an embedded type element (not a method), in a type parameter list the enclosinginterface{ … }
may be omitted for convenience:[T []P] // = [T interface{[]P}] [T ~int] // = [T interface{~int}] [T int|string] // = [T interface{int|string}] type Constraint ~int // illegal: ~int is not in a type parameter list
-
A type argument
T
satisfies a type constraintC
ifT
is an element of the type set defined byC
; i.e., ifT
implementsC
.As an exception, a strictly
comparable
type constraint may also be satisfied by a comparable (not necessarily strictly comparable) type argument. More precisely: A typeT
satisfies a constraintC
if-
T
implementsC
; or -
C
can be written in the forminterface{ comparable; E }
, whereE
is a basic interface andT
is comparable and implementsE
.
type argument type constraint // constraint satisfaction int interface{ ~int } // satisfied: int implements interface{ ~int } string comparable // satisfied: string implements comparable (string is strictly comparable) []byte comparable // not satisfied: slices are not comparable any interface{ comparable; int } // not satisfied: any does not implement interface{ int } any comparable // satisfied: any is comparable and implements the basic interface any struct{f any} comparable // satisfied: struct{f any} is comparable and implements the basic interface any any interface{ comparable; m() } // not satisfied: any does not implement the basic interface interface{ m() } interface{ m() } interface{ comparable; m() } // satisfied: interface{ m() } is comparable and implements the basic interface interface{ m() }
Because of the exception in the constraint satisfaction rule, comparing operands of type parameter type may panic at run-time (even though comparable type parameters are always strictly comparable).
-
-
A type parameter list declares the type parameters of a generic function or type declaration.
The type parameter list looks like an ordinary function parameter list except that the type parameter names must all be present and the list is enclosed in square brackets rather than parentheses.
TypeParameters = "[" TypeParamList [ "," ] "]" . TypeParamList = TypeParamDecl { "," TypeParamDecl } . TypeParamDecl = IdentifierList TypeConstraint .
-
All non-blank names in the type parameter list must be unique.
-
Each name declares a type parameter, which is a new and different named type that acts as a placeholder for an (as of yet) unknown type in the declaration.
-
The type parameter is replaced with a type argument upon instantiation of the generic function or type.
[P any] [S interface{ ~[]byte|string }] [S ~[]E, E any] [P Constraint[int]] [_ any]
-
-
As the ordinary function parameter has a parameter type, the type parameter has a (meta-)type which is called its type constraint.
-
7. Initialization
7.1. Constants
-
Constants are declared like variables, but with the
const
keyword. -
Constants cannot be declared using the
:=
syntax. -
Constants are created at compile time, even when defined as locals in functions, and can only be numbers, characters (runes), strings or booleans.
-
Because of the compile-time restriction, the expressions that define them must be constant expressions, evaluatable by the compiler.
-
In Go, enumerated constants are created using the
iota
enumerator.type Weekday int const ( Sunday Weekday = iota + 1 // iota: 0 ~ Sunday : 1 _ // iota: 1 ~ iota increased // comments // iota: 1 ~ skip: comment // iota: 1 ~ skip: empty line Monday // iota: 2 ~ Monday : 3 Tuesday // iota: 3 ~ Monday : 4 Wednesday // iota: 4 ~ Monday : 5 Thursday // iota: 5 ~ Monday : 6 Friday // iota: 6 ~ Monday : 7 Saturday // iota: 7 ~ Monday : 8 )
iota (noun) /aɪˈəʊtə/ /aɪˈəʊtə/ 1. [singular] (usually used in negative sentences) an extremely small amount There is not one iota of truth (= no truth at all) in the story. I don't think that would help one iota. 2. the 9th letter of the Greek alphabet (I, ι) ref: https://www.oxfordlearnersdictionaries.com/us/definition/english/iota
7.2. Allocation with new and make
-
Go has two allocation primitives, the built-in functions
new
andmake
. -
new
is a built-in function that allocates memory, but unlike its namesakes in some other languages it does not initialize the memory, it only zeros it.package builtin // import "builtin" func new(Type) *Type The new built-in function allocates memory. The first argument is a type, not a value, and the value returned is a pointer to a newly allocated zero value of that type.
-
That is,
new(T)
allocates zeroed storage for a new item of typeT
and returns its address, a value of type*T
. -
In Go terminology, it returns a pointer to a newly allocated zero value of type
T
. -
Since the memory returned by
new
is zeroed, it’s helpful to arrange when designing your data structures that the zero value of each type can be used without further initialization. This means a user of the data structure can create one withnew
and get right to work.For example, the documentation for
bytes.Buffer
states that "the zero value for Buffer is an empty buffer ready to use."
-
-
The built-in function
make(T, args)
serves a purpose different fromnew(T)
.package builtin // import "builtin" func make(t Type, size ...IntegerType) Type The make built-in function allocates and initializes an object of type slice, map, or chan (only). Like new, the first argument is a type, not a value. Unlike new, make's return type is the same as the type of its argument, not a pointer to it. The specification of the result depends on the type: Slice: The size specifies the length. The capacity of the slice is equal to its length. A second integer argument may be provided to specify a different capacity; it must be no smaller than the length. For example, make([]int, 0, 10) allocates an underlying array of size 10 and returns a slice of length 0 and capacity 10 that is backed by this underlying array. Map: An empty map is allocated with enough space to hold the specified number of elements. The size may be omitted, in which case a small starting size is allocated. Channel: The channel's buffer is initialized with the specified buffer capacity. If zero, or the size is omitted, the channel is unbuffered.
-
It creates slices, maps, and channels only, and it returns an initialized (not zeroed) value of type
T
(not*T
). -
The reason for the distinction is that these three types represent, under the covers, references to data structures that must be initialized before use.
var p *[]int = new([]int) // allocates slice structure; *p == nil; rarely useful var v []int = make([]int, 100) // the slice v now refers to a new array of 100 ints // Unnecessarily complex: var p *[]int = new([]int) *p = make([]int, 100, 100) // Idiomatic: v := make([]int, 100)
-
7.3. The init function
-
Each source file can define its own niladic
init
function to set up whatever state is required. -
Actually each file can have multiple init functions, which called in the order they appear in the source.
-
init
is called after all the variable declarations in the package have evaluated their initializers, and those are evaluated only after all the imported packages have been initialized.$ tree . ├── go.mod ├── hello │ └── hello.go ├── init.go └── world └── world.go 3 directories, 4 files $ cat go.mod module hello.world/init go 1.18 $ cat hello/hello.go package hello import "fmt" func init() { fmt.Printf("Hello") } $ cat world/world.go package world import "fmt" func init() { fmt.Printf(", ") } func init() { fmt.Printf("世界") } func init() { fmt.Printf("!\n") } $ cat init.go package main import ( _ "hello.world/init/hello" _ "hello.world/init/world" ) func main() { } $ go run init.go Hello, 世界!
7.4. Zero values
Variables declared without an explicit initial value are given their zero value.
The zero value is:
-
0
for numeric types, -
false
for the boolean type, -
""
(the empty string) for strings, -
nil
for the pointers, slices, maps, functions, interfaces, channels,
8. Concurrency
8.1. Race Conditions
-
A race condition is a situation in which the program does not give the correct result for some interleaving of the operations of multiple goroutines.
-
A data race, that is, a particular kind of race condition, occurs whenever two goroutines access the same variable concurrently and at least one of the accesses is a write. It follows from this definition that there are three ways to avoid a data race.
Azure Open AI | ChatGPT4
Other race conditions include deadlocks, livelocks, and starvation.
Deadlocks occur when two or more processes are waiting for each other to release resources that they need to continue executing.
Livelocks occur when two or more processes are constantly changing their state in response to the actions of the other process, without making any progress towards their goal.
A livelock is a situation where two or more processes or threads are actively trying to resolve a conflict but end up repeatedly yielding to each other, resulting in no progress being made. Livelocks are similar to deadlocks in that they both involve processes being stuck, but livelocks involve active, ongoing attempts to resolve the problem, whereas deadlocks involve processes waiting for resources.
Here’s an example of a livelock:
Imagine two people, Alice and Bob, walking in a narrow corridor towards each other. When they meet in the middle, they both step aside to let the other person pass. However, they both step in the same direction. Realizing this, they both step back to their original positions and try again, but they continue to step in the same direction each time. This results in a livelock, as they are both actively trying to resolve the situation but end up yielding to each other without making progress.
Starvation occurs when a process is prevented from accessing the resources it needs to execute, either because other processes are monopolizing those resources or because the system is not allocating resources fairly.
-
The first way is not to write the variable.
-
The second way (channels: share memory by communication) to avoid a data race is to avoid accessing the variable from multiple goroutines.
-
The third way (mutual exclusion:
sync.Mutex
,sync.RWMutex
) to avoid a data race is to allow many goroutines to access the variable, but only one at a time.
-
-
Synchronization is about more than just the order of execution of multiple goroutines; synchronization also affets memory.
8.2. Race Detector
-
The race detector (just add the
-race
flag to yourgo build
,go run
, orgo test
command) studies this steam of events, looking for cases in which one goroutine reads or writes a shared variables that was most recently written by a different goroutine without an intervening synchronization operation. -
The race detector reports all data races that wre actually executed.
However, it can only detect race conditions that occur during a run; it cannot prove that none will ever occur. 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
func main() { var wg sync.WaitGroup var x, y int wg.Add(1) go func() { defer wg.Done() x = 1 fmt.Printf("y = %d\n", y) }() wg.Add(1) go func() { defer wg.Done() y = 1 fmt.Printf("x = %d\n", x) }() wg.Wait() }
$ go run -race race.go x = 0 ================== WARNING: DATA RACE Write at 0x00c0000160c8 by goroutine 7: main.main.func1() /home/x/learn/go/race.go:15 +0xaa Previous read at 0x00c0000160c8 by goroutine 8: main.main.func2() /home/x/learn/go/race.go:23 +0xcf ... ================== ================== WARNING: DATA RACE Read at 0x00c0000160d8 by goroutine 7: main.main.func1() /home/x/learn/go/race.go:16 +0xcf Previous write at 0x00c0000160d8 by goroutine 8: main.main.func2() /home/x/learn/go/race.go:22 +0xaa ================== y = 1 Found 2 data race(s) exit status 66
Azure Open AI | ChatGPT4
When you run a Go program with the
For example, consider the following output:
This output can be interpreted as follows:
Using this information, you can identify the source code lines and goroutines involved in the race condition and take appropriate action to fix the issue, such as adding proper synchronization mechanisms like mutexes or channels. |
8.3. Happen before
-
Within a single goroutine, reads and writes must behave as if they executed in the order specified by the program.
-
That is, compilers and processors may reorder the reads and writes executed within a single goroutine only when the reordering does not change the behavior within that goroutine as defined by the language specification.
-
Because of this reordering, the execution order observed by one goroutine may differ from the order perceived by another.
For example, if one goroutine executes
a = 1; b = 2
;, another might observe the updated value ofb
before the updated value ofa
. -
To specify the requirements of reads and writes, we define happens before, a partial order on the execution of memory operations in a Go program.
If event e1 happens before event e2, then we say that e2 happens after e1.
Also, if e1 does not happen before e2 and does not happen after e2, then we say that e1 and e2 happen concurrently.
-
Within a single goroutine, the happens-before order is the order expressed by the program.
-
Programs that modify data being simultaneously accessed by multiple goroutines must serialize such access.
-
To serialize access, protect the data with channel operations or other synchronization primitives such as those in the
sync
andsync/atomic
packages.
8.4. Share by communicating
-
Do not communicate by sharing memory; instead, share memory by communicating.
-
Go encourages a different approach in which shared values are passed around on channels and, in fact, never actively shared by separate threads of execution.
-
Only one goroutine has access to the value at any given time. Data races cannot occur, by design.
-
-
One way to think about this model is to consider a typical single-threaded program running on one CPU.
-
It has no need for synchronization primitives.
-
Now run another such instance; it too needs no synchronization.
-
Now let those two communicate; if the communication is the synchronizer, there’s still no need for other synchronization.
-
Unix pipelines, for example, fit this model perfectly.
-
Although Go’s approach to concurrency originates in Hoare’s Communicating Sequential Processes (CSP), it can also be seen as a type-safe generalization of Unix pipes.
-
8.5. Goroutines
-
A goroutine has a simple model: it is a function executing concurrently with other goroutines in the same address space.
-
It is lightweight, costing little more than the allocation of stack space. [TALKSCONCURRENCY]
-
And the stacks start small, so they are cheap, and grow by allocating (and freeing) heap storage as required.
-
-
Goroutines are multiplexed onto multiple OS threads so if one should block, such as while waiting for I/O, others continue to run.
-
Their design hides many of the complexities of thread creation and management.
-
Prefix a function or method call with the
go
keyword to run the call in a new goroutine. When the call completes, the goroutine exits, silently. -
The evaluation of
f
,x
,y
, andz
ofgo f(x, y, z)
happens in the current goroutine and the execution off
happens in the new goroutine.func main() { // All function values created by this loop “capture” // and share the same variable—an addressable storage location, // not its value at that particular moment. for i := 0; i < 5; i++ { go func() { fmt.Print(i, " ") }() } time.Sleep(time.Millisecond) fmt.Println() for i := 0; i < 5; i++ { i := i go func() { fmt.Print(i, " ") }() } time.Sleep(time.Millisecond) // Output: // 5 5 5 5 5 // 4 0 1 2 3 // ignore the order }
-
Best number of goroutines in a process
There is no one-size-fits-all answer to the best number of goroutines in a process, as it depends on various factors such as the nature of your application, the resources available on your system, and the level of parallelism your program can achieve. However, here are some general guidelines to help you determine the optimal number of goroutines: [Azure Open AI | ChatGPT4]
-
I/O-bound tasks: If your application performs tasks that are primarily I/O-bound, such as reading from or writing to disk, network, or other external resources, you can benefit from a higher number of goroutines. Since I/O-bound tasks often involve waiting for external resources, having more goroutines can help keep your application busy and utilize available CPU resources effectively. In this case, the optimal number of goroutines might be several times the number of available CPU cores.
-
CPU-bound tasks: If your application performs tasks that are primarily CPU-bound, such as complex calculations or data processing, you may not benefit from a higher number of goroutines than the number of available CPU cores. Having more goroutines than CPU cores can lead to frequent context switching, which can hurt performance. In this case, the optimal number of goroutines might be close to the number of available CPU cores.
-
Workload characteristics: The best number of goroutines also depends on the specific characteristics of your application’s workload. For example, if your application has a mix of I/O-bound and CPU-bound tasks, or if it has varying resource requirements over time, you might need to experiment with different numbers of goroutines to find the optimal balance.
-
Resource availability: The optimal number of goroutines also depends on the resources available on your system, such as CPU, memory, and I/O capacity. If your system is constrained in terms of resources, you may need to limit the number of goroutines to avoid exhausting system resources and causing performance issues.
When there are many Go processes running on the same operating system, it is essential to consider the overall resource usage of the entire system. Running multiple Go processes with a large number of goroutines can lead to contention for system resources, such as CPU, memory, and I/O capacity. In this case, it might be necessary to limit the number of goroutines per process or to distribute the processes across multiple machines to ensure optimal performance.
Ultimately, the best way to determine the optimal number of goroutines for your application is through benchmarking, monitoring, and profiling. By measuring the performance of your application under different conditions and configurations, you can make informed decisions about the appropriate number of goroutines and resource allocation.
8.6. Channels
-
Like maps, channels are allocated with
make
, and the resulting value acts as a reference to an underlying data structure.-
If an optional integer parameter is provided, it sets the buffer size for the channel.
-
The default is zero, for an unbuffered or synchronous channel.
ci := make(chan int) // unbuffered channel of integers cj := make(chan int, 0) // unbuffered channel of integers cs := make(chan *os.File, 100) // buffered channel of pointers to Files
-
-
Receivers always block until there is data to receive.
-
The sender blocks only until the value has been copied to the buffer;
-
A buffered channel can be used like a semaphore, for instance to limit throughput.
-
The assembly line metaphor (pipeline) is useful one for channels and goroutines.
// A concurrent prime sieve // Send the sequence 2, 3, 4, ... to channel 'ch'. func Generate(ch chan<- int) { for i := 2; ; i++ { ch <- i // Send 'i' to channel 'ch'. } } // Copy the values from channel 'in' to channel 'out', // removing those divisible by 'prime'. func Filter(in <-chan int, out chan<- int, prime int) { for { i := <-in // Receive value from 'in'. if i%prime != 0 { out <- i // Send 'i' to 'out'. } } } // The prime sieve: Daisy-chain Filter processes. func main() { ch := make(chan int) // Create a new channel. go Generate(ch) // Launch Generate goroutine. for i := 0; i < 10; i++ { prime := <-ch print(prime, "\n") ch1 := make(chan int) go Filter(ch, ch1, prime) ch = ch1 } }
8.7. Parallelization
Be sure not to confuse the ideas of concurrency—structuring a program as independently executing components—and parallelism—executing calculations in parallel for efficiency on multiple CPUs.
Although the concurrency features of Go can make some problems easy to structure as parallel computations, Go is a concurrent language, not a parallel one, and not all parallelization problems fit Go’s model.
package runtime // import "runtime"
func NumCPU() int
NumCPU returns the number of logical CPUs usable by the current process.
The set of available CPUs is checked by querying the operating system at
process startup. Changes to operating system CPU allocation after process
startup are not reflected.
func GOMAXPROCS(n int) int
GOMAXPROCS sets the maximum number of CPUs that can be executing
simultaneously and returns the previous setting. If n < 1, it does not
change the current setting. The number of logical CPUs on the local machine
can be queried with NumCPU. This call will go away when the scheduler
improves.
8.8. Synchronization
In Go, synchronization is essential for ensuring safe concurrent execution of code and coordinating the activities of multiple goroutines. Go provides various synchronization primitives and constructs to facilitate communication between goroutines, protect access to shared resources, and manage concurrent execution. Here’s a comprehensive introduction to synchronization in Go: [Azure Open AI | ChatGPT4]
8.8.1. Channels
Channels are the primary synchronization mechanism in Go and are used for communication between goroutines. They allow goroutines to send and receive values, which can be used for signaling or passing data. Channels provide a way to synchronize the execution of goroutines and ensure that data is safely shared without race conditions.
// Creating a channel
ch := make(chan int)
// Sending a value to a channel
ch <- 42
// Receiving a value from a channel
value := <-ch
-
Semaphores in Go?
Go does not provid semaphores as a dedicated primitive like
sync.Mutex
. Instead, they can be implemented using buffered channels.Semaphores are used to control access to a limited number of resources or to limit the degree of concurrency for a group of goroutines.
Here’s an example of how to implement a semaphore using a buffered channel in Go:
const maxConcurrent = 3 func main() { var wg sync.WaitGroup // Create a buffered channel to act as a semaphore semaphore := make(chan struct{}, maxConcurrent) // Simulate 10 tasks that need to be executed concurrently for i := 1; i <= 10; i++ { wg.Add(1) go func(taskID int) { defer wg.Done() // Acquire the semaphore semaphore <- struct{}{} // Execute the task fmt.Printf("Task %d started\n", taskID) time.Sleep(1 * time.Second) fmt.Printf("Task %d completed\n", taskID) // Release the semaphore <-semaphore }(i) } wg.Wait() }
8.8.2. sync.Mutex
A Mutex (short for "mutual exclusion", /ˈmjuːtɛks/) is a synchronization primitive used to protect access to shared resources and ensure that only one goroutine can access the resource at a time.
Mutexes help prevent race conditions when multiple goroutines attempt to modify shared state concurrently.
// Defining a Mutex
var mu sync.Mutex
// Locking and unlocking a Mutex
mu.Lock()
// access shared resource
mu.Unlock()
8.8.3. sync.RWMutex
An RWMutex (short for "read-write mutex") is similar to a Mutex but allows multiple readers to access a shared resource simultaneously, as long as no writer is accessing it.
It provides two separate locks: a read lock and a write lock. Multiple goroutines can acquire read locks at the same time, but only one can acquire a write lock, and it must wait for all read locks to be released.
// Defining an RWMutex
var rwMu sync.RWMutex
// Locking and unlocking for reading
rwMu.RLock()
// read shared resource
rwMu.RUnlock()
// Locking and unlocking for writing
rwMu.Lock()
// write to shared resource
rwMu.Unlock()
8.8.4. sync.WaitGroup
A WaitGroup is a synchronization construct used to wait for a collection of goroutines to finish execution, that is often used when you start multiple goroutines to perform tasks concurrently and need to wait for all of them to complete before proceeding.
// Defining a WaitGroup
var wg sync.WaitGroup
// Incrementing the WaitGroup counter
wg.Add(1)
// Decrementing the WaitGroup counter (usually in a goroutine)
wg.Done()
// Waiting for all goroutines to finish
wg.Wait()
8.8.5. sync.Cond
A Cond (short for "condition") is a synchronization primitive used to wait for or signal conditions.
It is useful when one or more goroutines need to wait for a specific condition to be met before they can proceed.
Conds are often used with a Mutex or RWMutex to protect access to the shared state being tested in the condition.
// Defining a Cond with a Mutex
c := sync.NewCond(&sync.Mutex{})
// Waiting for a condition
c.L.Lock()
// check condition
c.Wait()
c.L.Unlock()
// Signaling a single waiting goroutine that the condition has been met
c.Signal()
// Broadcasting to all waiting goroutines that the condition has been met
c.Broadcast()
type Queue struct {
data []int
cond *sync.Cond
}
func NewQueue() *Queue {
return &Queue{
data: make([]int, 0),
cond: sync.NewCond(&sync.Mutex{}),
}
}
func (q *Queue) Enqueue(item int) {
q.cond.L.Lock()
q.data = append(q.data, item)
q.cond.L.Unlock()
q.cond.Signal() // Signal that an item has been added to the queue
}
func (q *Queue) Dequeue() int {
q.cond.L.Lock()
for len(q.data) == 0 {
q.cond.Wait() // Wait for an item to be added to the queue
}
item := q.data[0]
q.data = q.data[1:]
q.cond.L.Unlock()
return item
}
8.8.6. sync.Once
A Once is a synchronization construct used to ensure that a function is only executed once, regardless of how many goroutines attempt to call it, that is useful for initializing shared resources, such as global variables or singletons, in a concurrent environment.
// Defining a Once
var once sync.Once
// Executing a function only once, regardless of how many goroutines call it
once.Do(func() {
// initialize shared resource
})
8.8.7. sync.Map
A Map is a concurrent, thread-safe map implementation provided by the sync
package. It is designed for cases where the number of keys is large and their lifetimes are mostly unknown.
Unlike the built-in map type, sync.Map
provides safe concurrent access without requiring an additional synchronization mechanism like a Mutex.
// Defining a sync.Map
var m sync.Map
// Storing a value in sync.Map
m.Store("key", "value")
// Loading a value from sync.Map
value, ok := m.Load("key")
// Deleting a value from sync.Map
m.Delete("key")
// Iterating over sync.Map
m.Range(func(key, value interface{}) bool {
fmt.Printf("key: %v, value: %v\n", key, value)
return true
})
8.8.8. sync/atomic
package atomic // import "sync/atomic"
Package atomic provides low-level atomic memory primitives useful for
implementing synchronization algorithms.
These functions require great care to be used correctly. Except for special,
low-level applications, synchronization is better done with channels or the
facilities of the sync package. Share memory by communicating; don't
communicate by sharing memory.
9. Errors
-
Library routines must often return some sort of error indication to the caller.
-
Go’s multi-value return makes it easy to return a detailed error description alongside the normal return value.
-
It is good style to use this feature to provide detailed error information.
-
By convention, errors have type
error
, a simple built-in interface.type error interface { Error() string }
-
The simplest way to create an
error
is by callingerrors.New
, which return a newerror
for a given error message. -
Calls to
errors.New
are relatively infrequent because there’s a conveninent wrapper function,fmt.Errorf
, that does string formatting too. -
When feasible, error strings should identify their origin, such as by having a prefix naming the operation or package that generated the error.
For example, in
package image
, the string representation for a decoding error due to an unknown format is "image: unknown format". -
Callers that care about the precise error details can use a type switch or a type assertion to look for specific errors and extract details.
9.1. Panic
-
There is a built-in function
panic
that in effect creates a runtime unrecoverable error that will stop the program. -
The function takes a single argument of arbitrary type—often a string—to be printed as the program dies.
package builtin // import "builtin" func panic(v interface{}) The panic built-in function stops normal execution of the current goroutine. When a function F calls panic, normal execution of F stops immediately. Any functions whose execution was deferred by F are run in the usual way, and then F returns to its caller. To the caller G, the invocation of F then behaves like a call to panic, terminating G's execution and running any deferred functions. This continues until all functions in the executing goroutine have stopped, in reverse order. At that point, the program is terminated with a non-zero exit code. This termination sequence is called panicking and can be controlled by the built-in function recover.
9.2. Recover
-
When
panic
is called, including implicitly for runtime errors such as indexing a slice out of bounds or failing a type assertion,-
it immediately stops execution of the current function
-
and begins unwinding the stack of the goroutine,
-
running any deferred functions along the way.
-
If that unwinding reaches the top of the goroutine’s stack, the program dies.
-
-
However, it is possible to use the built-in function
recover
to regain control of the goroutine and resume normal execution. -
A call to
recover
stops the unwinding and returns the argument passed to panic.Because the only code that runs while unwinding is inside deferred functions, recover is only useful inside deferred functions.
func F() { panic("F: panic.") } func G() { defer func() { e := recover() if e != nil { fmt.Println("G: recover:", e) } }() F() } func main() { G() // Output: // G: recover: F: panic. }
10. Testing
-
The
go test
subcommand is a test driver for Go packages that are organized according to certain conventions. -
In a package directory, files whose names end with
_test.go
are not part of the package ordinarily built bygo build
but are a part of it when built bygo test
. -
Within
_test.go
files, four kinds of functions are treated specially: tests, fuzzs, benchmarks, and examples.-
A test function, which is a function whose name begins with
Test
exercises some program logic for correct behavior;go test
calls the test function and report the result, which is either PASS or FAIL. -
With fuzzing, random data is run against your test in an attempt to find vulnerabilities or crash-causing inputs.
-
A benchmark function has a name beginning with
Benchmark
and measures the performance of some operation;go test
reports the mean execution time of the operation. -
And an example function, whose name starts with
Example
, provides machine-checked documentation.
func Foo(s string) string { return s } func TestFoo(t *testing.T) { var tests = []struct { s string want string }{ {"Hello", "Hello"}, {"世界!", "世界!"}, } for _, test := range tests { if got := Foo(test.s); got != test.want { t.Errorf("foo(%q) == %q, want %q", test.s, got, test.want) } } } // Fuzz test func FuzzFoo(f *testing.F) { // Seed corpus addition f.Add("hello") // Fuzz target f.Fuzz(func(t *testing.T, s string) { // s string // Fuzzing arguments if got := Foo(s); got != s { t.Errorf("foo(%q) == %q, want %q", s, got, s) } }) } func BenchmarkFoo(b *testing.B) { for n := 0; n < b.N; n++ { } } func ExampleFoo() { fmt.Println("BAR") // Output: // BAR }
$ GO111MODULE=off go test PASS ok _/tmp/learn-notes 0.003s $ GO111MODULE=off go test -fuzz=Fuzz -fuzztime=3s fuzz: elapsed: 0s, gathering baseline coverage: 0/1 completed fuzz: elapsed: 0s, gathering baseline coverage: 1/1 completed, now fuzzing with 4 workers fuzz: elapsed: 3s, execs: 226192 (75387/sec), new interesting: 0 (total: 1) fuzz: elapsed: 3s, execs: 226192 (0/sec), new interesting: 0 (total: 1) PASS ok _/tmp/learn-notes 3.127s $ GO111MODULE=off go test -bench=.* goos: linux goarch: amd64 cpu: Intel(R) Core(TM) i5-10210U CPU @ 1.60GHz BenchmarkFoo-4 1000000000 0.5349 ns/op PASS ok _/tmp/learn-notes 0.605s
-
11. Modules
Go 1.11 and 1.12 include preliminary support for modules, Go’s new dependency management system that makes dependency version information explicit and easier to manage.[BLOGSMODULES]
GOPATH and GO111MODULE: New module changes in Go 1.16
The It’s still possible to build packages in
We plan to drop support for |
In Go, if an old package and a new package have the same import path, the new package must be backwards compatible with the old package. |
There is certainly a cost to needing to introduce a new name for each backwards-incompatible API change, but as the semver FAQ says, that cost should encourage authors to more clearly consider the impact of such changes and whether they are truly necessary. |
A module path is the canonical name for a module, declared with the module directive in the module’s A module path should describe both what the module does and where to find it. Typically, a module path consists of a repository root path, a directory within the repository (usually empty), and a major version suffix (only for major version 2 or higher). [REFMOD] |
-
A module is a collection of related Go packages that are versioned together as a single unit.
-
Modules record precise dependency requirements and create reproducible builds.
-
Most often, a version control repository contains exactly one module defined in the repository root.
-
Summarizing the relationship between repositories, modules, and packages:
-
A repository contains one or more Go modules.
-
Each module contains one or more Go packages.
-
Each package consists of one or more Go source files in a single directory.
-
-
Modules must be semantically versioned according to semver, usually in the form
v(major).(minor).(patch)
, such asv0.1.0
,v1.2.3
, orv1.5.0-rc.1
.-
The leading
v
is required. -
If using Git, tag released commits with their versions.
-
-
A module is defined by a tree of Go source files with a
go.mod
file in the tree’s root directory. -
A module declares its identity in its
go.mod
via themodule
directive, which provides the module path.-
The import paths for all packages in a module share the module path as a common prefix.
-
The module path and the relative path from the
go.mod
to a package’s directory together determine a package’s import path.
-
-
In Go source code, packages are imported using the full path including the module path.
go help modules
go help go.mod
go help module-private
go help goproxy
go env GOPROXY # https://proxy.golang.org,direct
go env -w GOPROXY=https://goproxy.cn,direct
go env GOPROXY # https://goproxy.cn,direct
go help gopath
12. Printf
Package fmt
implements formatted I/O with functions analogous to C’s printf and scanf.[PKGFMT]
12.1. The Printing verbs
%v the value in a default format
when printing structs, the plus flag (%+v) adds field names
%#v a Go-syntax representation of the value
%T a Go-syntax representation of the type of the value
%% a literal percent sign; consumes no value
type Pointer struct {
X int
Y int
}
func main() {
p := Pointer{3, 4}
fmt.Printf("%%v: %v\n", p)
fmt.Printf("%%+v: %+v\n", p)
fmt.Printf("%%#v: %#v\n", p)
fmt.Printf("%%T: %T\n", p)
}
// Output:
// %v: {3 4}
// %+v: {X:3 Y:4}
// %#v: main.Pointer{X:3, Y:4}
// %T: main.Pointer
%t the word true or false
%b base 2
%c the character represented by the corresponding Unicode code point
%d base 10
%o base 8
%O base 8 with 0o prefix
%q a single-quoted character literal safely escaped with Go syntax.
%x base 16, with lower-case letters for a-f
%X base 16, with upper-case letters for A-F
%U Unicode format: U+1234; same as "U+%04X"
func main() {
n := 1234
fmt.Printf("%%b: %b\n", n)
fmt.Printf("%%c: %c\n", n)
fmt.Printf("%%d: %d\n", n)
fmt.Printf("%%o: %o\n", n)
fmt.Printf("%%O: %O\n", n)
fmt.Printf("%%q: %q\n", n)
fmt.Printf("%%x: %x\n", n)
fmt.Printf("%%X: %X\n", n)
fmt.Printf("%%U: %U\n", n)
}
// Output:
// %b: 10011010010
// %c: Ӓ
// %d: 1234
// %o: 2322
// %O: 0o2322
// %q: 'Ӓ'
// %x: 4d2
// %X: 4D2
// %U: U+04D2
%b decimalless scientific notation with exponent a power of two, in the manner of strconv.FormatFloat with the 'b' format, e.g. -123456p-78 %e scientific notation, e.g. -1.234456e+78 %E scientific notation, e.g. -1.234456E+78 %f decimal point but no exponent, e.g. 123.456 %F synonym for %f %g %e for large exponents, %f otherwise. Precision is discussed below. %G %E for large exponents, %F otherwise %x hexadecimal notation (with decimal power of two exponent), e.g. -0x1.23abcp+20 %X upper-case hexadecimal notation, e.g. -0X1.23ABCP+20
func main() {
n := -123456.789
fmt.Printf("%%b: %b\n", n)
fmt.Printf("%%e: %e\n", n)
fmt.Printf("%%E: %E\n", n)
fmt.Printf("%%f: %f\n", n)
fmt.Printf("%%F: %F\n", n)
fmt.Printf("%%g: %g\n", n)
fmt.Printf("%%G: %G\n", n)
fmt.Printf("%%x: %x\n", n)
fmt.Printf("%%X: %X\n", n)
}
// Output:
// %b: -8483885939586761p-36
// %e: -1.234568e+05
// %E: -1.234568E+05
// %f: -123456.789000
// %F: -123456.789000
// %g: -123456.789
// %G: -123456.789
// %x: -0x1.e240c9fbe76c9p+16
// %X: -0X1.E240C9FBE76C9P+16
%s the uninterpreted bytes of the string or slice
%q a double-quoted string safely escaped with Go syntax
%x base 16, lower-case, two characters per byte
%X base 16, upper-case, two characters per byte
%p address of 0th element in base 16 notation, with leading 0x
%p base 16 notation, with leading 0x
The %b, %d, %o, %x and %X verbs also work with pointers,
formatting the value exactly as if it were an integer.
%v
is:bool: %t
int, int8 etc.: %d
uint, uint8 etc.: %d, %#x if printed with %#v
float32, complex64, etc: %g
string: %s
chan: %p
pointer: %p
struct: {field0 field1 ...}
array, slice: [elem0 elem1 ...]
maps: map[key1:value1 key2:value2 ...]
pointer to above: &{}, &[], &map[]
'+' always print a sign for numeric values;
guarantee ASCII-only output for %q (%+q)
'-' pad with spaces on the right rather than the left (left-justify the field)
'#' alternate format: add leading 0b for binary (%#b), 0 for octal (%#o),
0x or 0X for hex (%#x or %#X); suppress 0x for %p (%#p);
for %q, print a raw (backquoted) string if strconv.CanBackquote
returns true;
always print a decimal point for %e, %E, %f, %F, %g and %G;
do not remove trailing zeros for %g and %G;
write e.g. U+0078 'x' if the character is printable for %U (%#U).
' ' (space) leave a space for elided sign in numbers (% d);
put spaces between bytes printing strings or slices in hex (% x, % X)
'0' pad with leading zeros rather than spaces;
for numbers, this moves the padding after the sign;
ignored for strings, byte slices and byte arrays
func main() {
n := 123
fmt.Printf("%+06d\n", n)
}
// Output:
// +00123
12.2. Width and Precision
-
Width is specified by an optional decimal number immediately preceding the verb.
If absent, the width is whatever is necessary to represent the value.
-
Precision is specified after the (optional) width by a period followed by a decimal number.
If no period is present, a default precision is used. A period with no following number specifies a precision of zero.
%f default width, default precision %9f width 9, default precision %.2f default width, precision 2 %9.2f width 9, precision 2 %9.f width 9, precision 0
12.3. Stringer
type Stringer interface {
String() string
}
Stringer
is implemented by any value that has a String
method, which defines the "native" format for that value.
The String
method is used to print values passed as an operand to any format that accepts a string or to an unformatted printer such as Print
.
// Animal has a Name and an Age to represent an animal.
type Animal struct {
Name string
Age uint
}
// String makes Animal satisfy the Stringer interface.
func (a Animal) String() string {
return fmt.Sprintf("%v (%d)", a.Name, a.Age)
}
func main() {
a := Animal{
Name: "Gopher",
Age: 2,
}
fmt.Println(a)
// Output:
// Gopher (2)
}
References
-
[BLOGMAPS] https://go.dev/blog/maps
-
[BLOGSMODULES] https://go.dev/blog/using-go-modules
-
[BLOGSTRINGS] https://go.dev/blog/strings
-
[REFMOD] https://go.dev/ref/mod
-
[CAPITERVAR] Capturing Iteration Variables in Go Language
-
[CHANNELCLOSING] https://groups.google.com/g/golang-nuts/c/pZwdYRGxCIk/m/qpbHxRRPJdUJ
-
[DAVEPRACTICALGO] Practical Go: Real world advice for writing maintainable Go programs
-
[EFFECTIVEGO] https://go.dev/doc/effective_go.html
-
[ERRORS] Errors in Go language
-
[GOCHANNELS] Goroutines and Channels in Go Lanugage
-
[GOTESTING] Testing in Go Language2686 * [] https://semver.org/
-
[INITSO] https://stackoverflow.com/questions/24790175/when-is-the-init-function-run
-
[MEDIUMGOMODULES] https://medium.com/@adiach3nko/package-management-with-go-modules-the-pragmatic-guide-c831b4eaaf31
-
[PKGFMT] https://pkg.go.dev/fmt
-
[REFMEM] https://go.dev/ref/mem
-
[SHAREDVAR] Concurrency with Shared Variables in Go Language
-
[SPECIOTA] https://go.dev/ref/spec#Iota
-
[TALKSCONCURRENCY] https://talks.golang.org/2012/concurrency.slide
-
[TOURGENERICS] https://go.dev/tour/generics/1
-
[VGOIMPORT] https://research.swtch.com/vgo-import
-
[VGOMODULE] https://research.swtch.com/vgo-module
-
[VGOMVS] https://research.swtch.com/vgo-mvs
-
[WIKIIOTA] https://github.com/golang/go/wiki/Iota
-
[WIKIMODULES] https://github.com/golang/go/wiki/Modules