Types
Booleans
- zero initialized when declared without a value
- cannot be modified by functions, unless when using a pointer
1
2
| var zeroInitializedBool bool // false
var valueBool bool = true
|
Integers
- zero initialized when declared without a value
- cannot be modified by functions, unless when using a pointer
1
2
3
4
5
6
7
8
9
10
11
12
13
| var zeroInitializedInt int // 0
var valueInt int = 5
// Other integer types have a minimum and maximum value
var min8, max8 int8 = -128, 127
var min16, max16 int16 = -32768, 32767
var min32, max32 int32 = -2147483648, 2147483647
var min64, max64 int64 = -9223372036854775808, 9223372036854775807
var min_u8, max_u8 uint8 = 0, 255
var min_u16, max_u16 uint16 = 0, 65535
var min_u32, max_u32 uint32 = 0, 4294967295
var min_u64, max_u64 uint64 = 0, 18446744073709551615
|
Decimals (float)
- zero initialized when declared without a value
- cannot be modified by functions, unless when using a pointer
1
2
3
4
5
| var max_f32 float32 = 3.40282346638528859811704183484516925440e+38
var smallest_f32 float32 = 1.401298464324817070923729583289916131280e-45
var max_f64 float64 = 1.797693134862315708145274237317043567981e+308
var smallest_f64 float64 = 4.940656458412465441765687928682213723651e-324
|
Runes
- zero initialized when declared without a value
- cannot be modified by functions, unless when using a pointer
1
2
3
4
5
6
7
| var r1 rune = 'A'
// a line break
var line_break rune = '\n'
// a unicode checkmark symbol ✓
var checkmark rune = '\u2713'
|
Pointers
1
2
3
| var value int = 5
var pointer *int = &value
println(value == *pointer)
|
Arrays
- zero initialized when declared without a value
- cannot be modified by functions, unless when using a pointer
1
2
3
4
5
6
7
8
9
10
11
| var arrayOfZeros [5]int
var array = [5]int{0, 1, 2, 10: 10, 11}
// note the colon : syntax to initialize the element at index 10
nElementsCopied := copy(dst, src)
length := len(array)
firstElement = array[0]
lastElement = array[len(array)-1]
fullSlice = array[:]
subSlice = array[3:5]
|
Slices
Slices are dynamically allocated, variable-length arrays.
- They are default initialized to
nil
. - Their values can be modified in function, with caveats: Their length, capacity and memory location cannot be modified in functions because they are passed by value. However, the value of elements can be modidied by functions since they are accessed through the pointer.
Their implementation can be illustrated as follows:
1
2
3
4
5
| struct slice {
length int
capacity int
memory *T
}
|
Slices Cheatsheet:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| var nilSlice []int // == nil
var emptySlice = []int{} // length=0, capacity=0
var sliceWithValues = []int{
0, 1, 2, 3,
10: 10,
}
var sliceWithLength = make([]int, length)
var sliceWithLengthAndCapacity = make([]int, length, capacity)
firstElement = slice[0]
length := len(slice)
slice = append(slice, element1, element2)
slice = append(slice, ...otherSlice)
yesno = slices.Equal(slice1, slice2)
yesno = slice1 == nil
nElementsCopied := copy(dst, src)
|
Maps
Implemented with pointers:
- They are default initialized to
nil
, - Their values can be modified in function, with caveats.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| var nilMap map[string]int // == nil
var emptyMap = map[string]int{}
var mapWithValues = map[string]int{
"https://takia.dev": 100,
}
var mapWithLength = make(map[string]int, length)
// read
v, ok := emptyMap["hello"]
if !ok {
// ...
}
delete(mapWithValues, "key")
maps.Equal(map1, map2)
|
Control Structures
If statements
1
2
3
4
5
6
7
8
| a := 5
if a == 5 {
// ...
} else if a > 5 {
// ...
} else {
// ...
}
|
If statements can use if-local variables as follows:
1
2
3
4
5
6
| if n := rand.Intn(10); n < 5 {
// n exists in this block
} else {
// n exists in this block too
}
// n does not exist outside the if block
|
Switch statements
Classic switch:
1
2
3
4
5
6
| switch value {
case 0, 1, 2:
// ...
default:
// ...
}
|
Empty switch:
1
2
3
4
5
6
7
8
9
10
| value := 0
switch {
case value < 5:
// ...
case value > 5:
// ...
default:
// ...
}
|
Switch-local variables:
1
2
3
4
5
6
7
8
9
| switch value := 5; {
case value < 5:
// ...
}
switch value := 5; value {
case 0, 1, 2:
// ...
}
|
For loops
Classic C-style for loop:
1
2
| for i := 0; i < 10; i++ {
}
|
Condition-only for loop:
1
2
3
4
| i := 0
for i < 10 {
i += 1
}
|
Sequence iteration:
1
2
3
4
| s := []string{"a", "b", "c"}
for index, value := range s {
println(index, value)
}
|
Map iteration:
1
2
3
4
| m := map[string]string{"key1": "value1"}
for key, value := range m {
println(key, value)
}
|
String iteration (as array of rune
):
1
2
3
4
5
| str := "Hello 😊"
for index, rune := range str {
// for iterates over runes, not bytes
println(index, rune)
}
|
Labeled for loops allow using break
or continue
on an outer loop from an inner loop:
1
2
3
4
5
6
7
8
| // labeled for loop
outer:
for i_outer := range []int{1, 2, 3, 4} {
for i_inner := range []int{1, 2, 3} {
unused(i_outer, i_inner)
continue outer
}
}
|
Goto statements
Goto statements can be use to skip over blocks of code. Although it should only be sparingly used, it can sometimes come handy for error handling. Use of defer
is preferred when possible.
1
2
3
4
5
6
7
8
| jumpOver := true
if jumpOver {
goto skip
}
println("This statement will be skipped")
skip:
// ...
|
Functions
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
|
func simpleFunction(arg1 int, arg2 int) int {
return arg1 + arg2
}
func genericFunction[T any](arg1 T) T {
return arg1
}
func variadicFunction(vals ...int) int {
result := 0
for _, v := range vals {
result += v
}
return result
}
func multipleReturnValues() (int, error) {
return 0, nil
}
func namedReturnValues() (retA int, err error) {
// defer registers statements to be executed at exit
// named return values allows defer to modify them
defer func() {
if err != nil {
retA = 0
// rollback code
}
}()
return 1, nil
}
|
Functions can be stored in variables:
1
2
3
4
5
6
| // Function variables
var myfunc func(int, int) int = simpleFunction
// Function types
type FuncType func(int, int) int
var myfunc2 FuncType = simpleFunction
|
Anonymous Function can access outer variables:
1
2
3
4
5
6
7
8
9
|
func f() {
outerVariable := 5
setValue := func(arg int) {
outerVariable = arg
}
setValue(7)
println(outerVariable)
}
|
User-Defined types
User-Defined types
User-Defined type can be declared as follows:
1
| type NewType ExistingType
|
There is no implicit cast between user-defined types. The code belows shows how this can be exploited to ensure that every string has been escaped before it is displayed on a webpage:
1
2
3
4
5
6
7
8
9
10
| type EscapedString string
func safeEscape(s string) EscapedString {
escaped := s //html.EscapeString(s)
return EscapedString(escaped)
}
func displayOnPage(s EscapedString) {
// ...
}
|
Structs
Structs are passed by value, they cannot be modified by functions unless when using a pointer. Struct fields are default initialized to their respective zero values.
1
2
3
4
5
6
7
8
9
| type person struct {
name string
age int
}
var structOfZeros person // == {"", 0}
var julien = person{"Julien", 30}
var alisha = person{name: "Alisha"}
alisha.age = 30
|
Methods & Member functions
Methods can be defined on user-defined types as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| type person struct {
name string
age int
}
// value-receiver methods cannot modify the instance
func (p person) String() string {
return fmt.Sprintf("%s (age:%d)", p.name, p.age)
}
// pointer-receiver methods can modify the instance
func (p *person) GrowOld() {
// handle case where p == nil!
p.age += 1
}
|
Go will automatically reference or dereference values and pointer to match the value-receiver method or pointer-receiver method. But remember that value-receiver methods work on a copy of the object and therefore should not attempt to modify its member values.
1
2
3
4
5
6
7
| p1 := person{"Julien", 30}
p1.String()
p1.GrowOld() // auto reference
p2 = &p1
p2.String() // auto dereference
p2.GrowOld()
|
Type Embeddings
Type embeddings embed one InnerType
inside an OuterType
and make methods defined on InnerType
available on instances of OuterType
:
For instance, the given Inner
type:
1
2
3
4
5
6
7
| type Inner struct {
innerField int
}
func (i Inner) ShowInnerField() {
println(i.innerField)
}
|
Can be embedded inside an Outer
type as follows:
1
2
3
4
5
6
7
8
9
10
11
| type Outer struct {
Inner
outerField int
}
o := Outer{
Inner: Inner{
innerField: 5,
},
outerField: 10,
}
|
And the Inner
method can be called on the Outer
object:
Embedding merge the method set of Inner
and Outer
making it possible to implement interfaces more easily. But beware that:
Embeddings are not inheritance:
- they do not provide implicit slicing or type cast from derived type to parent type
- they do not provide dynamic dispatch and method resolution
Interfaces
Interfaces specify what the caller code needs, they define the set of method than an object is expected to offer. Interfaces are similar to python’s Protocols: unlike in Java they are implemented implicitly.
1
2
3
| type Provider interface {
ProviderID() string
}
|
Any object with a Read() string
method automatically implements the Reader
interface.
1
2
3
4
5
6
| type MyProvider struct {
name string
}
func (p MyProvider) ProviderID() string {
return p.name
}
|
The following function requests a Provider
instance and can be used with our MyProvider
object implicitely:
1
2
3
4
5
6
| func useProvider(p Provider) {
// ...
}
var p = MyProvider{"name"}
useProvider(p)
|
Interface Embeddings
Interfaces can be merged together using interface embeddings:
1
2
3
4
5
6
7
| type Reader interface{}
type Writer interface{}
type ReadWriter interface {
Reader
Writer
}
|
nil Interfaces
Under the hood, interface are implemented with two pointers: one for the instance and one for the instance type:
1
2
3
4
| type Interface[T any] struct {
type *Type[T]
value *T
}
|
An interface is considered nil
if it has not been assigned a type. In the code below, even though nilInstance
is set to the nil
pointer, the nilInterface
object is assigned the Object
type and therefore does not evaluate to nil
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
type Interface interface {
Method()
}
type Object struct{}
func (o Object) Method() {
}
var nilInstance *O
var notNilInterface I = nilInstance
println("Instance is nil:", nilInstance == nil) // true
println("Interface is nil:", notNilInterface == nil) // false
|
A common pitfall is to pre-declare the error variable to return at the end of a function as in this snippet:
1
2
3
4
5
6
7
8
9
10
| type CustomError struct {}
func (c CustomError) Error() string { return "message" }
func brokenFunction(arg int) error {
var err CustomError; // nil CustomError
if arg < 0 {
err = CustomError{}
}
return err // BUG! The error interface is not nil
}
|
The function would then be used as follows:
1
2
3
4
| var err error = brokenFunction(5)
if err != nil {
println("ERROR")
}
|
Which will always enter the error block because the error
interface does not evaluate to nil
. (Its type pointer is assigned to CustomError
!)
To avoid this pitfall, declare variables of the error
type instead of the custom error type.
Enums
Go uses iota
, a special counter whose value increases for each constant in a const block. It can be used to assign increasing values to consecutive const values. Note that it is possible to use _
to disable the default value.
1
2
3
4
5
6
7
| type EnumType int
const (
EnumDefaultValue EnumType = iota
EnumValue1
EnumValue2
EnumValue3
)
|
Generics
A function or type is said generic when it accepts type arguments. Type arguments are passed between square brackets []
. In the code below, the generic type argument T
implements the comparable
constraint. Any interface can be used as a type constraint.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| type Set[T comparable] struct {
storage map[T]bool
}
func (s *Set[T]) add(value T) {
if s.storage == nil {
s.storage = map[T]bool{}
}
s.storage[value] = true
}
func (s *Set[T]) remove(value T) {
delete(s.storage, value)
}
func (s Set[T]) contains(value T) bool {
return s.storage[value]
}
var s = Set[int]{}
s.add(5)
println("Set contains 5?", s.contains(5))
s.remove(5)
|