The purpose of this article is to give you a quick introduction to the Go language, so that you can spend about ten minutes reading through the whole article and have a preliminary knowledge of Go, and lay the foundation for further in-depth learning of Go.

This article assumes that you have no contact with Go at all. You may be a programmer who is proficient in other programming languages, or you may be a person who has no programming experience and just wants to become a programmer.

Introduction to Programming

Programming is the process of producing programs that can be executed on a computer. In this process, the programmer is the “labor”, the programming language is the tool, and the executable program is the result. The Go language is a great production tool for programmers to use in the programming production process.

What the programmer, as the “workforce”, does in this process is to use some programming language as a production tool to organize and express the pre-designed execution logic, much like a writer organizes and puts on paper the storyline designed in his or her brain in human language.

By this analogy, learning a programming language is like learning a human language, where vocabulary and syntax are the main things we learn. This article will provide a quick explanation of the main “vocabulary” and syntax forms of the Go language.

Go Introduction

Go is a new back-end programming language developed by three great programmers at Google, Robert Griesemer, Rob Pike and Ken Thompson, in 2007, and in 2009, Go was announced as open source.

Golang features easy-to-learn, static typing, fast compilation, efficient operation, clean code, and native support for concurrent programming. It also supports automatic memory management, which allows developers to focus more on programming itself without worrying about memory leaks. In addition, Golang also supports multi-core processors, which can better take advantage of the advantages of multi-core processors and improve the running efficiency of programs.

After more than a decade of development, Go is now a popular programming language that can be used to develop a variety of applications, including web applications, web services, system management tools, mobile applications, game development, database management, etc. Go is commonly used to build large distributed systems and to build high-performance server-side applications. Go has developed a number of “killer” applications for the current era of cloud-native computing, including Docker, Kubernetes, Prometheus, InfluxDB, Cillum, and others.

Installing Go

Go is a static language that needs to be compiled and then executed, so before we can develop Go programs, we first need to install the Go compiler and the associated toolchain. The steps to install are simple.

  • Download the latest version of Golang installer from Go official website - https://go.dev/dl/
  • Unzip the installation package and copy it to the location where you want to install it, e.g. /usr/local/go; for Windows, MacOS platforms, you can also download the installation package for graphical installation.
  • Setting environment variables to add the Golang installation path to the PATH variable.
  • Open a terminal, type go version, and check if the Go language is installed successfully. If the output is similar to the following, the installation is successful!
1
2
$go version
go version go1.20 darwin/amd64

First Go program: Hello World

Create a new directory and create a new file helloworld.go in it. Open helloworld.go with any editor and type the following Go source code.

1
2
3
4
5
6
7
8
9
//helloworld.go

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

Go supports running a source file directly.

1
2
$go run helloworld.go
Hello, World!

But usually we compile this source file (helloworld.go) first to generate an executable binary (./helloworld) and then run it.

1
2
3
$go build -o helloworld helloworld.go
$./helloworld
Hello, World!

Go Packages

A Go package organizes a set of Go language source files into a reusable unit for use in other Go programs. All source files belonging to the same Go package are placed in a single directory, and by convention the name of that directory is the same as the package name. Take the io package of the Go standard library as an example, the list of source files in its package is as follows.

1
2
3
4
// List of files in the  $GOROOT/src/io directory
io.go
multi.go
pipe.go

Go packages are also the basic unit of Go compilation. The Go compiler can compile packages as executable files (if the package is a main package and contains a main function implementation) or as reusable library files (.a).

Package declarations

Go packages are usually declared at the beginning of each Go source file, using the keyword package.

1
2
3
4
// mypackage.go
package mypackage

... ...

Package names by convention are usually single words or abbreviations in all lowercase, such as io, net, os, fmt, strconv, bytes, etc.

Importing a Go package

To use an existing Go package, we need to import the package in the source code. To import a Go package, you can use the import keyword, for example.

1
2
3
4
5
6
7
8
import "fmt"                    // Importing fmt packages from the standard library

import "github.com/spf13/pflag" // Importing the spf13 open source pflag package

import _ "net/http/pprof"       // importing the standard library net/http/pprof package.
                                // but does not explicitly use the identifiers of types, variables, functions, etc. in the package

import myfmt "fmt"              // Rename the imported package to myfmt

Go modules

Go module (module) is a new feature introduced in version 1.11 of the Go language. A Go module is a collection of related Go packages that are treated as a separate unit for uniform version management.

Go module is a new dependency management mechanism that makes it easier for developers to manage dependencies in Go language projects and allows better support for multiple versions of dependencies. In Go projects of practical value, we use Go modules for dependency management; Go modules are versioned, and Go module versioning dependencies are based on strict adherence to semantic versioning (semver).

Go uses the go.mod file to accurately document dependency requirements. Here’s how to manipulate dependencies in go.mod.

1
2
3
4
5
6
7
8
9
$go mod init demo // Create a go.mod with a module root of demo
$go mod init github.com/bigwhite/mymodule // Create a go.mod with a module root of github.com/bigwhite/mymodule

$go get github.com/bigwhite/foo@latest  // Add a dependency package github.com/bigwhite/foo to go.mod, using the latest version
$go get github.com/bigwhite/foo         // Equivalent to the above command
$go get github.com/bigwhite/foo@v1.2.3  // Explicitly specify to get v1.2.3

$go mod tidy   // Automatically add missing dependencies and clean up unused ones
$go mod verify //Confirm that all dependencies are valid

Go Minimum Project Structure

Go does not officially specify a standard structure layout for Go projects. Here is the Go minimum project structure recommended by Russ Cox, technical lead of the Go core team.

1
2
3
4
5
6
7
8
// In the Go project repository root path

- go.mod
- LICENSE
- README
- xx.go
- yy.go
... ...

or

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// In the Go project repository root path

- go.mod
- LICENSE
- README
- package1/
    - package1.go
- package2/
    - package2.go
... ...

Variables

Golang has two types of variable declarations.

  • Using the var keyword

    The declaration using the var keyword is suitable for all situations.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    var a int     // Declare an int variable a with an initial value of 0
    var b int = 5 // Declare an int variable b with an initial value of 5
    var c = 6     // Go will automatically assign a default type to the variable c based on the value on the right, the default integer type is int
    
    var (         // We can place variable declarations uniformly in a var block, which is equivalent to the way they were declared above.
        a int
        b int = 5
        c = 6
    )
    

    Note: Go variable declarations use variable names first and types second, which is quite different from static programming languages such as C, C++, and Java.

  • Declare variables using short declarations

    1
    2
    
    a := 5       // Declare a variable a. Go will automatically assign a default type to the variable a based on the value on the right, and the default integer type is int
    s := "hello" // Declare a variable s. Go will automatically assign a default type to the variable s based on the value on the right, and the default string type is string
    

    Note: This declaration is limited to use within functions or methods and cannot be used to declare package level variables or global variables.

Constants

Constants in Go language are declared using the const keyword.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const a int       // Declare an int-type constant a with the value 0
const b int = 5   // Declare an int-type constant b with the value 5
const c = 6       // Declare a constant c. Go will automatically assign a default type to the constant c based on the value on the right, and the default integer type is int
const s = "hello" // Declare a constant s. Go will automatically assign a default type to the constant s based on the value on the right, and the default string type is string

const (           // We can place constant declarations uniformly in a const block, which is equivalent to the declaration above
    a int
    b int = 5
    c = 6
    s = "hello"
)

Types

Golang has a variety of built-in basic and composite types.

Basic types

The basic types natively supported by Go include boolean, numeric (integer, floating point, complex), and string types, here are some examples.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
bool  // Boolean type, default value false

uint     // Architecture-dependent unsigned integer, whose length is 8 bytes on 64-bit platforms
int      // Architecture-dependent signed integer, whose length is 8 bytes on 64-bit platforms
uintptr  // The architecture-related type used to represent the value of a pointer, which is an unsigned integer large enough to store the value of a pointer of any type

uint8    // Architecture-independent 8-bit unsigned integer
uint16   // Architecture-independent 16-bit unsigned integer
uint32   // Architecture-independent 32-bit unsigned integers
uint64   // Architecture-independent 64-bit unsigned integers

int8     // Architecture-independent 8-bit signed integer
int16    // Architecture-independent 16-bit signed integers
int32    // Architecture-independent 32-bit signed integers
int64    // Architecture-independent 64-bit signed integers

byte     // Aliases of type uint8
rune     // Alias of type int32 for a unicode character

float32     // Single precision floating point type, meets IEEE-754 specification
float64     // Double precision floating point type, meets IEEE-754 specification

complex64   // Complex type, its real and imaginary parts are float32 floating point type
complex128  // Complex type, its real and imaginary parts are float64 floating point type

string      // String type, default value is ""

We can use the predefined function complex to construct the complex type, for example: complex(1.0, -1.4) constructs the complex number as 1 - 1.4i.

Composite types

Golang natively supports composite types including arrays, slices, structures, pointers, functions, interfaces, maps, and channels.

Array type

go Array type

An array type is a continuum of elements of the same type, which has a fixed length and cannot be dynamically expanded.

1
2
3
4
[8]int      // An array type with elements of type int and length 16
[32]byte    // An array type with elements of type byte and length 32
[2]string   // An array with elements of type string and length 2
[N]T        // An array type with elements of type T and length N

The length of the array can be obtained with the predefined function len.

1
2
var a = [8]int{11, 12, 13, 14, 15, 16, 17, 18}
println(len(a)) // 8

Use the array index (starting from 0) to directly access any element in the array.

1
2
3
println(a[0]) // 11
println(a[2]) // 13
println(a[7]) // 1

Go supports declaring multidimensional arrays, i.e., the element type of the array is still the array type.

1
[2][3][5]float64  // A multidimensional array type, equivalent to [2]([3]([5]float64))

Slice Type

golang Slice Type

A slice type is similar to an array type in that it is also a continuum of elements of the same type. The difference is that the slice type has variable length and we do not need to pass the length attribute when declaring the slice type.

1
2
3
4
[]int       // A slice type with element type int
[]string    // A slice type with element type string
[]T         // A slice type with element type T
[][][]float64 // Multidimensional slice type, equivalent to []([]([]float64))

The current length of the slice can be obtained with the predefined function len.

1
2
var sl = []int{11, 12} // A slice of element type int, whose length (len) is 2, and whose value is [11 12]
println(len(sl)) // 2

The slice has another property, which is capacity, and its capacity value can be obtained through the predefined function cap.

1
2
3
4
sl = append(sl, 13)     //Append new element to sl, after operation sl is [11 12 13]
sl = append(sl, 14)     //Append new element to sl, after operation sl is [11 12 13 14]
sl = append(sl, 15)     //Append new element to sl, after operation sl is [11 12 13 14 15]
println(len(sl), cap(sl)) // 5 8 Slicing capacity automatically expands to 8 after appending

Like arrays, slices use indexes to directly access the elements in them.

1
2
3
println(sl[0]) // 11
println(sl[2]) // 13
println(sl[4]) // 1

Structure Types

Go’s struct type is an aggregate of different types of fields that provides a generic, aggregated abstraction of entity objects. The following is a structure type containing three fields.

1
2
3
4
5
struct {
    name string
    age  int
    gender string
}

We usually give a name to such a structure type, such as Person below.

1
2
3
4
5
type Person struct {
    name string
    age  int
    gender string
}

A variable of type Person is declared below.

1
2
3
4
5
var p = Person {
    name: "tony bai",
    age: 20,
    gender: "male",
}

We can access the fields in the structure via p.FieldName.

1
2
println(p.name) // tony bai
p.age = 21

The definition of a structure type T can contain field members of type *T, but cannot recursively contain field members of type T.

1
2
3
4
5
type T struct {
    ... ...
    p *T    // ok
    t T     // Error: recursive definition
}

Go structs can also have other types embedded in their definitions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type F struct {
    ... ...
}

type MyInt int

type T struct {
    MyInt
    F
    ... ...
}

The name of the embedded type will be used as the field name.

1
2
3
4
5
6
7
8
var t = T {
    MyInt: 5,
    F: F {
        ... ...
    },
}

println(t.MyInt) // 5

Go supports empty structs that do not contain any fields.

1
2
struct{}
type Empty struct{}        // An empty structure type

The empty struct type has a size of 0, which is useful in many scenarios (eliminating the overhead of memory allocation).

1
2
var t = Empty{}
println(unsafe.Sizeof(t)) // 0

Pointer type

The pointer type for int type is *int, and the pointer type for T type is *T. Unlike non-pointer types, pointer type variables store the address of a memory cell, and the size of *T pointer type variables is not related to the size of the T type, but to the length of the system address representation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
*int     // An int pointer type
*[4]byte // A [4]byte array pointer type

var a = 6
var p *T // Declare a pointer variable p of type T, with the default value of nil
p = &a   // Assign a value to a pointer variable p using the memory address of variable a
*p = 7   // Pointer dereference, changing the value of variable a from 6 to 7 via pointer p

n := new(int)  // The predefined function returns a pointer of type *int
arr := new([4]int)  // Allocate a [4]int array using the predefined function new and return a pointer of type *[4]int

map types

map is an abstract data type provided by Go language which represents a set of unordered key-value pairs, a set of map types are defined below.

1
2
3
map[string]int                // A map type with a key type of string and a value type of int
map[*T]struct{ x, y float64 } // A map type with key type *T and value type struct{ x, y float64 }
map[string]interface{}        // A map type with key type string and value type interface{}

We can create an instance of a map type with a map literal or make.

1
2
3
var m = map[string]int{}      // Declare a variable of type map[string]int and initialize it
var m1 = make(map[string]int) // Equivalent to the above statement
var m2 = make(map[string]int, 100) // Declare a variable of type map[string]int and initialize it with the recommended initial capacity of 100

The method of manipulating map variables is also very simple.

1
2
3
m["key1"] = 5  // Add/set a key-value pair
v, ok := m["key1"]  // Get the value of the key "key1", if it exists, then its value is stored in v, ok is true
delete(m, "key1") // Delete the key "key1" and its corresponding value from the map

Other types

The function, interface, and channel types are described in detail later.

Custom types

Custom types can be implemented using the type keyword.

1
2
3
4
5
6
7
8
type T1 int         // Define a new type T1, whose underlying type is int
type T2 string      // Define a new type T2, whose underlying type is string
type T3 struct{     // Define a new type T3, whose underlying type is a structure type
    x, y int
    z string
}
type T4 []float64   // Define a new type T4 with the underlying type []float64 slice type
type T5 T4          // Define a new type T5, whose underlying type is T4 type

Go also supports defining aliases for types, in the following form.

1
2
3
type T1 = int       // Define the type alias of int as T1, which is equivalent to int
type T2 = string    // Define the type alias of string as T2, T2 is equivalent to string
type T3 = T2        // Define the type alias of T as T3, which is equivalent to T2 and also to string

Type conversions

Go does not support implicit auto-transformation. To perform a type conversion operation, we must do it explicitly, even if the underlying types of the two types are the same.

1
2
3
4
5
6
7
type T1 int
type T2 int
var t1 T1
var n int = 5
t1 = T1(n)      // Explicitly convert int type variables to T1 type
var t2 T2
t2 = T2(t1)     // Explicit conversion of T1 variables to T2 types

Many of Go’s native types support interconversion.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Interconversion of numeric types

var a int16 = 16
b := int32(a)
c := uint16(a)
f := float64(a)

// Slicing and array conversions (supported in Go 1.17 and later)

var a [3]int = [3]int([]int{1,2,3}) // Slice-to-array conversion
var pa *[3]int = [3]int([]int{1,2,3}) // Slice to array pointer conversion
sl := a[:] // Converting arrays to slices

// Interconversion of strings and slices

var sl = []byte{'h', 'e','l', 'l', 'o'}
var s = string(sl) // s is hello
var sl1 = []byte(s) // sl1 is ['h' 'e' 'l' 'l' 'o']
string([]rune{0x767d, 0x9d6c, 0x7fd4})  // []rune slices to string conversion

Control Statements

Go provides common control statements, including conditional branching (if), looping statements (for), and selective branching statements (switch).

Conditional branch statements

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// if ...

if a == 1 {
    ... ...
}

// if - else if - else

if a == 1 {

} else if b == 2 {

} else {

}

// Self-use variables with conditional statements
if a := 1; a != 0 {

}

// Nested if statements

if a == 1 {
    if b == 2 {

    } else if c == 3 {

    } else {

    }
}

Loop statement

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Classic Loop

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

// Simulate while ... do

for i < 10 {

}

// Infinite loop

for {

}

// for range

var s = "hello"
for i, c := range s {

}

var sl = []int{... ...}
for i, v := range sl {

}

var m = map[string]int{}
for k, v := range m {

}

var c = make(chan int, 100)
for v := range c {

}

Select branch statement

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
var n = 5
switch n {
    case 0, 1, 2, 3:
        s1()
    case 4, 5, 6, 7:
        s2()
    default: // Default branch
        s3()
}

switch n {
    case 0, 1:
        fallthrough  // Explicitly tells the action of the following branch to be executed
    case 2, 3:
        s1()
    case 4, 5, 6, 7:
        s2()
    default:
        s3()
}

switch x := f(); {
    case x < 0:
        return -x
    default:
        return x
}

switch {
    case x < y:
        f1()
    case x < z:
        f2()
    case x == 4:
        f3()
}

Functions

Go uses the func keyword to declare a function.

1
2
3
func greet(name string) string {
    return fmt.Sprintf("Hello %s", name)
}

A function consists of a function name, a list of optional arguments, and a list of return values. go functions support the return of multiple return values, and we usually place the return type indicating the error value at the end of the return value list.

1
2
3
4
func Atoi(s string) (int, error) {
    ... ...
    return n, nil
}

Functions are first-class citizens in Go, so functions themselves can be used as parameters or return values.

1
2
3
4
5
func MultiplyN(n int) func(x int) int {
  return func(x int) int {
    return x * n
  }
}

An anonymous function such as func(x int) int defined in the MultiplyN function above, whose implementation references its outer function MultiplyN with parameter n, is also known as a closure.

When we talk about functions, we cannot fail to mention defer, a function F called with defer in front of it, the execution of the function F will be “postponed” until the end of its caller A.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func F() {
    fmt.Println("call F")
}

func A() {
    fmt.Println("call A")
    defer F()
    fmt.Println("exit A")
}

func main() {
    A()
}

The output of the above example is as follows.

1
2
3
call A
exit A
call F

It is possible to use defer multiple times in a function.

1
2
3
4
5
func B() {
    defer F()
    defer G()
    defer H()
}

The functions modified by defer will be called in the order of “first in, last out” at the end of the B function. The output of the above B function after execution is as follows.

1
2
3
call H
call G
call F

Methods

Methods are functions with receivers. Here is a method Length of type Point.

1
2
3
4
5
6
7
type Point struct {
    x, y float64
}

func (p Point) Length() float64 {
    return math.Sqrt(p.x * p.x + p.y * p.y)
}

The part between the func keyword and the function name is the receiver, which is also the link between the Length method and the Point type. We can call the Length method from the Point type variable.

1
2
var p = Point{3,4}
fmt.Println(p.Length())

It is also possible to use methods as functions.

1
2
var p = Point{3,4}
fmt.Println(Point.Length(p)) // This usage is also known as method expression

Interfaces

An interface is a collection of methods that represents a “contract”. The following is the interface type of a collection of methods consisting of three methods.

1
2
3
4
5
type MyInterface interface {
    M1(int) int
    M2(string) error
    M3()
}

Go promotes interface-oriented programming because through interfaces we can easily build low-coupling applications.

Go also supports nesting other interface types (e.g. io.Writer, sync.Locker) within an interface type (e.g. I), with the result that the set of methods of the new interface type I is the concatenation of the set of its methods with the set of methods of the embedded interface types Writer and Locker.

1
2
3
4
type I interface { //An interface type with other interface types embedded
   io.Writer
   sync.Locker
}

Interface implementation

If a type T implements all the methods in the method collection of some interface type MyInterface, then we say that the type T implements the interface MyInterface, and so the variable t of type T can be assigned to the variable i of the interface type MyInterface, at which point the dynamic type of the variable i is T.

1
2
var t T
var i MyInterface = t // ok

The methods of T can be called by the above variable i.

1
2
3
i.M1(5)
i.M2("demo")
i.M3()

An interface type with an empty set of methods, interface{}, is called an “empty interface type”, and a blank “contract” means that any type implements the empty interface type, i.e., any variable can be assigned to a variable of type interface{}. type.

1
2
3
4
5
var i interface{} = 5 // ok
i = "demo"            // ok
i = T{}               // ok
i = &T{}              // ok
i = []T{}             // ok

Note: The new predefined identifier any, introduced in Go 1.18, is the equivalent type to interface{}.

Type Assertion for Interfaces

Go supports type assertion to extract the value of an interface variable from its dynamic type.

1
v, ok := i.(T) // Type Assertion

If the dynamic type of the interface variable i is indeed T, then v will be given the value of that dynamic type with an ok value of true; otherwise, v will be a zero value of type T with an ok value of false.

Type assertions also support the following form of syntax.

1
v := i.(T)

However, in this form, the statement will throw a panic once the interface variable i has been previously given a value other than a value of type T.

Type switch for interface types

“type switch” is a special use of the switch statement, used only for interface type variables.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func main() {
    var x interface{} = 13
    switch x.(type) {
    case nil:
        println("x is nil")
    case int:
        println("the type of x is int") // Execute this branch case
    case string:
        println("the type of x is string")
    case bool:
        println("the type of x is string")
    default:
        println("don't support the type")
    }
}

The switch keyword is followed by the expression x.(type). This form of expression is exclusive to switch statements and can only be used in switch statements. The x in this expression must be an interface type variable, and the result of the expression is the dynamic type corresponding to the interface type variable.

The expression after switch in the above example can also be changed from x.(type) to v := x.(type). v will store information about the value corresponding to the dynamic type of variable x.

1
2
3
4
5
6
7
8
var x interface{} = 13
switch x.(type) {
    case nil:
        println("v is nil")
    case int:
        println("the type of v is int, v =", v) // Execute this branch case, v = 13
    ... ...
}

Generic

Go supports generics since version 1.18. The basic syntax of Go generics is type parameter (type parameter), and the essence of the Go generic scheme is the support for type parameters, including the following.

  • generic function: a function with type parameters.
  • generic type: a custom type with type arguments.
  • generic method: a method of a generic type.

Generic functions

The following is the definition of a generic function max.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type ordered interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
        ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
        ~float32 | ~float64 |
        ~string
}

func max[T ordered](sl []T) T {
    ... ...
}

Compared with ordinary Go functions, the max function has an extra code enclosed by square brackets between the function name and the function argument list: [T ordered]; the argument type in the max argument list and the return value type in the return value list are both T, not a specific type.

The extra [T ordered] in the max function is the list of type parameters (type parameters list) of the Go generic, and in the example there is only one type parameter T in this list, ordered is the type constraint of the type parameter (type constraint).

We can call generic functions like normal functions, and we can explicitly specify the actual parameter type of the generic.

1
2
var m int = max[int]([]int{1, 2, -4, -6, 7, 0})  // Explicitly specify the generic parameter type as int
fmt.Println(m) // 7

Go also supports automatic inference of the actual parameter type of a generic type.

1
2
var m int = max([]int{1, 2, -4, -6, 7, 0}) // Automatically inferring T as int
fmt.Println(m) // 7

Generic types

A generic type is a Go type with a type parameter in the type declaration.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type Set[T comparable] map[T]string

type element[T any] struct {
    next *element[T]
    val  T
}

type Map[K, V any] struct {
  root    *node[K, V]
  compare func(K, K) int
}

Take the generic type Set as an example, its usage is as follows.

1
2
3
var s = Set[string]{}
s["key1"] = "value1"
println(s["key1"]) // value1

Generic methods

Go types can have their own methods, and generic types are no exception. The methods defined for generic types are called generic methods.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Set[T comparable] map[T]string

func (s Set[T]) Insert(key T, val string) {
    s[key] = val
}

func (s Set[T]) Get(key T) (string, error) {
    val, ok := s[key]
    if !ok {
        return "", errors.New("not found")
    }
    return val, nil
}

func main() {
    var s = Set[string]{
        "key": "value1",
    }
    s.Insert("key2", "value2")
    v, err := s.Get("key2")
    fmt.Println(v, err) // value2 <nil>
}

Type constraints

Go sets restrictions on the type parameters of generic functions and the implementation code in generic functions by means of type constraints (constraint). go uses the extended syntax post-interface type to define constraints.

The following is an example of using a regular interface type as a constraint

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type Stringer interface {
    String() string
}

func Stringify[T fmt.Stringer](s []T) (ret []string) { // The actual parameters of T are constrained by Stringer to be only types that implement the Stringer interface
    for _, v := range s {
        ret = append(ret, v.String())
    }
    return ret
}

The Go interface type declaration syntax has been extended to support putting type element information in the interface type.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
type ordered interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
        ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
        ~float32 | ~float64 | ~string
}

func Less[T ordered](a, b T) bool {
    return a < b
}

type Person struct {
    name string
    age  int
}

func main() {
    println(Less(1, 2)) // true
    println(Less(Person{"tony", 11}, Person{"tom", 23})) // Person does not satisfy the ordered constraint and will result in a compilation error
}

Concurrency

Go does not use OS threads as the basic execution unit for concurrency. Instead, it implements goroutine, a lightweight user-level thread that is scheduled by the Go runtime (runtime), to provide native support for concurrent programming.

goroutine

We can create a goroutine by using the go keyword + function/method. once created, the new goroutine will have a separate code execution flow and will be dispatched by the Go runtime along with the goroutine that created it.

1
2
3
4
5
go fmt.Println("I am a goroutine")

// $GOROOT/src/net/http/server.go
c := srv.newConn(rw)
go c.serve(connCtx)

After the goroutine’s execution function returns, the goroutine exits. If the main goroutine (the goroutine that executes main.main) exits, then the entire Go application process will exit and the program life cycle will end.

channel

Go provides a native mechanism for communication between goroutines, channel, which is defined and operated as follows.

1
2
3
4
5
6
7
8
9
// channel type
chan T          // A channel type with element type T
chan<- float64  // A send-only channel type with element type float64
<-chan int      // An element of type int receives only channel types

var c chan int             // Declare a variable of type channel with element type int and an initial value of nil
c1 := make(chan int)       // Declare an unbuffered channel variable with element type int
c2 := make(chan int, 100)  // Declare a buffered channel variable of element type int with a buffer size of 100
close(c)                   // Close a channel

Here is an example of two goroutines communicating based on a channel.

1
2
3
4
5
6
7
func main() {
    var c = make(chan int)
    go func(a, b int) {
        c <- a + b
    }(3,4)
    println(<-c) // 7
}

Go provides the select mechanism when it comes to operating on multiple channels at the same time. With select, we can send/receive operations on multiple channels at the same time.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
select {
case x := <-ch1:     // Receive data from channel ch1
  ... ...

case y, ok := <-ch2: // Receive data from channel ch2, and determine if ch2 is closed based on the ok value
  ... ...

case ch3 <- z:       // Send the z-value to channel ch3:
  ... ...

default:             // When none of the channel communications in the case above can be implemented, the default branch is executed
}

Error handling

Go provides a simple, error handling mechanism based on the comparison of error values. This mechanism makes it mandatory for every developer to explicitly attend to and handle each error.

error type

Go uses the interface type error to represent errors, and by convention we usually put the error type return value at the end of the return value list.

1
2
3
4
// $GOROOT/src/builtin/builtin.go
type error interface {
    Error() string
}

Any instance of a type that implements the Error method of error can be assigned as an error value to the error interface variable.

Go provides convenient methods for constructing error values.

1
2
err := errors.New("your first demo error")
errWithCtx = fmt.Errorf("index %d is out of bounds", i)

Error handling forms

The most common forms of error handling for Go are as follows.

1
2
3
4
5
err := doSomething()
if err != nil {
    ... ...
    return err
}

Usually we define some “sentinel” error values to assist the error handler in inspecting the error values and making decisions on error handling branches.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// $GOROOT/src/bufio/bufio.go
var (
    ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")
    ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune")
    ErrBufferFull        = errors.New("bufio: buffer full")
    ErrNegativeCount     = errors.New("bufio: negative count")
)

func doSomething() {
    ... ...
    data, err := b.Peek(1)
    if err != nil {
        switch err {
        case bufio.ErrNegativeCount:
            // ... ...
            return
        case bufio.ErrBufferFull:
            // ... ...
            return
        case bufio.ErrInvalidUnreadByte:
            // ... ...
            return
        default:
            // ... ...
            return
        }
    }
    ... ...
}

Is and As

Starting with Go 1.13, the standard library errors package provides the Is function for error handler review of error values. the Is function is similar to comparing an error type variable to a “sentinel” error value.

1
2
3
4
// Similar  if err == ErrOutOfBounds{ … }
if errors.Is(err, ErrOutOfBounds) {
    // Out-of-bounds error handling
}

The difference is that if the underlying error value of the error type variable is a Wrapped Error, the errors.Is method goes down the Error Chain where the Wrapped Error is located and compares it with all the Wrapped Errors in the chain until it finds a match.

The standard library errors package also provides the As function for error handlers to review error values. as function is similar to determining whether an error type variable is a specific custom error type by type assertion.

1
2
3
4
5
// Similar if e, ok := err.(*MyError); ok { … }
var e *MyError
if errors.As(err, &e) {
    // If the err type is *MyError, the variable e will be set to the corresponding error value
}

If the dynamic error value of the error type variable is a wrapped error, the errors.As function goes down the error chain where the wrapped error is located and compares it with the types of all the wrapped errors in the chain until it finds a matching error type, just as the errors.Is function does.

Summary

By reading this, you have an entry-level knowledge of the Go language, but further study and practice is needed to become a Gopher (the name given to Go developers).

Ref

  • https://tonybai.com/2023/02/23/learn-go-in-10-min/