Contents

Code With Go

Go language (also known as Golang) is an open-source programming language developed by Google and first publicly released in 2009. It is designed for building efficient, reliable, and concise software. One of the design goals of Go is to provide a language with high concurrency, high performance, and ease of writing, suitable for application development in various domains.

Go language includes built-in support for concurrent programming through goroutines and channels, making it easy to create highly concurrent programs. It employs automatic garbage collection, relieving developers from manual memory management. The syntax of Go is simple and clear, enhancing readability and writability. Go comes with a rich standard library covering various functionalities including networking, file operations, encryption, concurrency, and more. Due to its excellent performance and concurrency support, Go has been widely adopted for building web applications, backend services, distributed systems, and more.

This document will explain the main syntax and features of the Go language.

1 Package

Packages play a crucial role in the Go language. They are used to organize related functions, structures, constants, and variables into a cohesive unit of functionality. The modular nature of packages enhances code readability, maintainability, and reusability. In a Go program, the special package is called the “main” package. A Go program must include a “main” package, containing the entry function main(), which is the first function executed when the program runs.

// main.go
package main

import "fmt"

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

By using packages, you can better organize and manage your Go code, resulting in clearer and more modular code structures.

2 Variables and Functions

In the Go language, keywords are used to declare variables and functions.

The var keyword is used to declare variables, while the func keyword is used to declare functions. When declaring variables, you can choose whether to initialize the variable’s value. When declaring functions, you need to specify the function name, parameter list, and function body. In Go, functions can return multiple values and can be passed as values, allowing for anonymous functions and more.

Naming conventions for variables and functions follow the camel case style. The visibility of a variable or function is determined by the capitalization of the first letter: identifiers that start with a capital letter are visible to external packages, while those starting with a lowercase letter are not. Inside a function, you can use the := shorthand to declare and initialize variables.

var num int = 5

func sum(newNum int) int {
    retVal := newNum + num
    return retVal
}

Go is a statically typed language, which means that variables must be declared with their types. Go’s type system is strict; variable types are determined at compile time and cannot be changed at runtime. While you can use the short variable declaration (:=) to let the compiler infer the variable type when declaring variables, explicit use of the var keyword is required for variables declared outside of functions.

Go has 13 basic types.

2.1 Integer Types

  • int: Depending on the architecture, it can be either 32 or 64 bits.
  • int8, int16, int32, int64: Represent signed integers with 8, 16, 32, or 64 bits respectively.
  • uint, uint8, uint16, uint32, uint64: Represent unsigned integers.
var intValue int = 10
var uintValue uint = 20

2.2 Floating-Point Types

float32, float64: Represent 32-bit and 64-bit floating-point numbers respectively.

var floatValue float32 = 3.14
var doubleValue float64 = 6.28

2.3 Complex Types

  • complex64: Represents a complex number with two float32 components.
  • complex128: Represents a complex number with two float64 components.
var complexValue complex64 = 1 + 2i
var biggerComplexValue complex128 = 2 + 3i

2.4 String Type

string: Represents a sequence of characters.


var stringValue string = "Hello, Go!"
  • The len function is used to get the length of a string.
  • The == and != operators can be used to compare two strings for equality.
  • The + operator can be used to concatenate two strings.
  • The strings.Split function splits a string into a slice based on a specified delimiter.
  • The strings.Replace function replaces occurrences of a substring in a string.
  • The strings.Contains function checks if a string contains a specified substring.
  • The strings.HasPrefix and strings.HasSuffix functions check if a string starts or ends with a specified prefix or suffix.
  • The strings.ToLower and strings.ToUpper functions convert a string to lowercase or uppercase.

2.5 Boolean Type

bool: Represents a boolean value, which can be either true or false.

var boolValueTrue bool = true
var boolValueFalse bool = false

2.6 Pointer Type

*T: Represents a pointer to a value of type T.

var intPointer *int
intPointer = &intValue

2.7 Array Type

In Go, arrays are fixed-size data structures with elements of the same type, used to store an ordered collection of data. Once the size of an array is determined, it cannot be changed. When declaring an array, you need to specify its length and element type.

[n]T: Represents an array of n elements, where each element is of type T.

var intArray [3]int
intArray = [3]int{1, 2, 3}
  • The len function is used to get the length of an array.

  • The range keyword is used to iterate over an array, providing both the index and value.

    for index, value := range arr {
        fmt.Printf("Index: %d, Value: %d\n", index, value)
    }
    
  • The == operator can be used to compare two arrays for equality.

Arrays in Go are fundamental data structures suitable for storing fixed-size collections of data. However, because arrays have a fixed size, slices (dynamic arrays) are more commonly used when dealing with dynamically sized collections of data. Slices are built on top of arrays and offer greater flexibility.

2.8 Slice Type

In Go, a slice is a dynamically sized data structure built on top of arrays, allowing automatic resizing. Slices provide convenient methods for manipulating sequential data, making them more flexible and efficient for dealing with dynamically sized collections. When a slice’s length exceeds its capacity, the slice is automatically resized, which involves reallocating a larger underlying array.

[]T: Represents a dynamically sized slice, where each element is of type T.

In Go, the make function is used to create dynamic data structures such as slices, maps, and channels. The make function returns an initialized and usable instance of the data structure. For slices, the make function requires specifying the slice type and length, and optionally the capacity. The length of a slice is the number of elements it currently holds, while the capacity is the size of the underlying array. A slice may be reallocated if its length exceeds its capacity, using a larger underlying array.

In the case of maps and channels, the make function is used primarily to initialize new instances for use. For array types, you don’t need to use the make function; arrays are declared by initializing them directly. The make function is only used for creating dynamic data structures.


var intSlice = make([]int, 5)

Slices can be initialized from arrays or other slices.

var intArray = [5]int{1, 2, 3, 4, 5}
var intSlice = intArray[:]
  • The append function is used to add elements to the end of a slice.
  • The len function is used to get the length of a slice, and the cap function is used to get its capacity.

Slices can share the same underlying array, allowing for efficient memory usage and data sharing among slices.

2.9 Map Type

map[K]V: Represents a map (dictionary) with keys of type K and values of type V.

var keyValueMap map[string]int
keyValueMap = map[string]int{"one": 1, "two": 2}
  • The delete function is used to remove key-value pairs from a map.
  • The expression _, ok := map[key] is used to check if a key exists in a map (ok will be true if the key exists).
  • The range keyword is used to iterate over key-value pairs in a map.
  • The len function is used to get the number of key-value pairs in a map.

2.10 Function Type

func: Represents a function type, which can be a parameter or a return value type.

var addFunction func(int, int) int
addFunction = func(a, b int) int {
    return a + b
}

In Go, if a function accepts a pointer type parameter, passing a value of that type automatically creates a pointer to the value and passes it to the function. This conversion is implicit; developers don’t need to manually create pointers. The same applies in reverse: if a function accepts a non-pointer type parameter, passing a pointer to that type automatically dereferences the pointer and passes the pointed-to value.

2.11 Struct Type

struct: Represents a composite data type that can contain fields of different types.

type Rectangle struct {
    Width  float64
    Height float64
}
var rect Rectangle = Rectangle{Width: 16.0, Height: 8.0}

2.11.1 Methods

In Go, methods are functions associated with a specific type, and they can be called on instances of that type. Methods can be used to add behavior and functionality to user-defined types (structs), allowing these types to have their own behaviors. Method definitions are similar to regular function definitions, but they require a receiver type in front of the method name to indicate with which type the method is associated.

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

With a pointer receiver type, methods can modify the values of the instance.

func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

rect := &Rectangle{Width: 5, Height: 3}
rect.Scale(2)

2.12 Interface Type

Interfaces play a vital role in programming. They enable polymorphism, allowing objects of different types to be treated uniformly. Through interfaces, you can write generic code that handles objects of different types without concerning the specific types. This enhances code reuse and flexibility. Interfaces also separate the definition of an interface from its implementation, reducing coupling between different modules. Modules can be developed based on interface definitions without needing to know the implementation details, which improves maintainability and extensibility. For example, consider a graphics computation program that calculates the area and perimeter of different shapes. If using interfaces, you could define a Shape interface with methods for calculating area and perimeter. Then, you could create types (e.g., rectangles, circles) that implement the Shape interface. This allows you to use the same method names to handle different shapes, improving flexibility and maintainability.

In Go, an interface is a special data type that defines a collection of method signatures. An interface describes the behavior that objects should exhibit, without concerning their specific types. By implementing an interface, different types can have the same method set, achieving polymorphism and code reuse. In Go, an interface is a set of method signatures; it does not include the implementation code for methods. A method signature consists of the method name, parameter list, and return types.

type Shape interface {
    Area() float64
    Perimeter() float64
}

Any type that implements all the methods in an interface is considered to implement that interface. Interface implementation is implicit; there’s no need to explicitly declare it. In the example below, Rectangle implements the Shape interface.

type Rectangle struct {
    Width  float64
    Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

2.13 Channel Type

chan T: Represents a channel, used for communication between goroutines.

var intChannel chan int
intChannel = make(chan int)

3 Handling errors

The error mechanism in the Go programming language is based on returning values. It employs a special built-in type error to represent errors and encourages developers to handle errors in a general way.

3.1 error Type

error is a built-in interface type that is often returned by functions to indicate errors that occurred during execution. It has only one method, Error(), which is used to return a description of the error.

type error interface {
    Error() string
}

3.1.1 Custom error

To create a custom error type, you usually define a struct with the name of that type. The struct needs to satisfy the error interface by implementing the Error() string method.

// Custom error type
type MyError struct {
    message string
}

// Implement the Error() method of the error interface
func (e MyError) Error() string {
    return e.message
}

3.1.2 Using error

Functions can indicate whether their execution was successful by returning an error. If the function succeeds, it typically returns nil. If an error occurs, it returns a non-nil error value.

func function() error {
    if someCondition {
        return nil
    } else {
    	return MyError{"Something went wrong"}    
    }
}

The errors package is a standard library package used for creating and handling errors. It provides functions for creating simple error values and for checking error types.

  • The errors package offers a New function for creating a new error value. This is useful for creating simple error messages.

    import "errors"
    
    func function() error {
        return errors.New("Something went wrong")
    }
    
  • You can use functions from the errors package to check whether an error is of a specific type.

    errors.Is(err, target) is used to check if an error matches a target error type. This function returns a boolean indicating whether the given error is an instance or wrapper of the target error type.

    import "errors"
    
    err := function()
    if err != nil {
        if errors.Is(err, io.EOF) {
            // Handle EOF error
        }
    }
    
  • Go supports error chaining, allowing you to wrap one error within another to provide more context.

    fmt.Errorf is a function provided by the Go standard library’s fmt package. It’s used to format and create a new error. It behaves similarly to creating errors with string formatting, and you can wrap one error within another using the %w placeholder to provide more context.

    if err := function(); err != nil {
        return fmt.Errorf("encountered an error: %w", err)
    }
    
  • You can retrieve the description of an error using the error’s Error() method. This is useful for logging and presenting user-friendly error messages.

    import "fmt"
    
    err := function()
    if err != nil {
        fmt.Println("Error:", err)
    }
    

    When passing an object that implements the error interface to functions like fmt.Println, they automatically invoke the object’s Error() method to retrieve the error’s description and print it.

3.2 panic and recover

3.2.1 defer

defer is a keyword used to defer the execution of a function until the surrounding function returns. This ensures that the deferred function is executed whether the function returns normally or panics. defer is commonly used for cleanup tasks like closing files or releasing resources to ensure they are executed before the function exits.

defer function(arguments)
  • If multiple defer statements are used in a function, they are executed in reverse order, with the last deferred function being executed first, and so on.
  • Deferred functions are executed after the surrounding function has finished executing, even if a return statement is encountered.

3.2.2 panic + recover

panic and recover are mechanisms used to handle exceptional situations in a program. They are typically used for handling unrecoverable errors and exceptions.

panic is a built-in function used to indicate a serious error from which the program cannot recover. When panic is called, the current function’s execution stops immediately, the program exits the current function’s call stack, and then it starts executing the defer functions in each call frame. If no recover is encountered, the program terminates with a runtime error.

panic can be triggered intentionally by developers or occur due to uncontrollable errors (such as array out-of-bounds, null pointer dereference, etc.).

func main() {
    panic("Something went wrong")
}

recover is a built-in function used within defer functions to capture the panic caused by a panic call. It allows the program to recover and continue. recover must be called inside a defer to be effective. When called, it returns the value passed to panic, or nil if no panic occurred.

Usually, recover is used to regain control of the program’s flow, enabling cleanup operations or resuming partial logic.

func main() {
    // Trigger a panic, causing an interruption; but due to the following defer function, the program won't terminate
    panic("Something went wrong")
    
    // Define an anonymous function using defer, to be executed at the end of the main function
    defer func() {
        // Use recover within this anonymous function to catch the panic
        if r := recover(); r != nil {
            fmt.Println("Recovered:", r)
        }
    }()
}

3.3 Error Handling Examples

In Go, it’s common to write functions that return two values, one of which is of type error. This pattern is used to indicate the success of a function’s execution and provide error information in case of failure.

package main

import (
    "errors"
    "fmt"
)

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

func main() {
    result, err = divide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Result:", result)
    }
}

3.4 Considerations

  • Avoid using only if err != nil for error checks. Errors can be other values too, and specific error handling might be needed.
  • Avoid simply using _ or ignoring errors, as this could hide potential issues and impact the robustness of your program.
  • Unless it’s an unrecoverable situation, avoid using panic. It can lead to an uncontrolled program termination instead of graceful error handling.

4 Flow Control

Go language’s flow control includes conditional statements (if, switch), loops (for), and jump statements (break, continue, goto). In Go, flow control statements are used similarly to other programming languages, but Go emphasizes simplicity and readability. All conditional statements are written without parentheses.

func main {
	// if
    x := 10
    if x > 5 {
        fmt.Println("x is greater than 5")
    } else if x == 5 {
        fmt.Println("x is equal to 5")
    } else {
        fmt.Println("x is less than 5")
    }

    // switch
    day := "Monday"
    switch day {
    case "Monday":
        fmt.Println("It's the start of the week")
    case "Friday", "Saturday":
        fmt.Println("It's the weekend")
    default:
        fmt.Println("It's a regular day")
    }
    
	// for
    for i := 0; i < 5; i++ {
        fmt.Println(i)
    }    
}

5 Concurrency

Go language was designed with concurrency in mind, offering lightweight concurrency support that makes writing concurrent programs simple and efficient. Go uses a mechanism called goroutine for concurrency, along with channels for synchronization and communication.

5.1 Goroutine

A goroutine is a concurrent execution unit in Go, similar to a lightweight thread. Unlike traditional threads, goroutine creation and switching have minimal overhead, allowing thousands of goroutines to run concurrently. The go keyword is used to start a new goroutine.

func main() {
    go doSomething()
}

Go language employs the CSP (Communicating Sequential Processes) concurrency model. It abstracts concurrency using independent goroutines that communicate and synchronize through channels. This model makes writing concurrent code more intuitive and safe. Go also provides mechanisms for safe concurrent access, such as mutexes and read-write locks. These mechanisms help developers avoid race conditions and data races when multiple goroutines access shared data.

5.2 Concurrency Communication

A channel is a conduit for data exchange between different goroutines. Channels are mainly used for data exchange and synchronization between goroutines. They help address the challenges of concurrent access to shared data, ensuring synchronization and consistency. Channels also enable communication and coordination among goroutines.

The make function is used to create a channel, specifying the data type it will carry. Channels can be closed using the close function. After a channel is closed, data cannot be sent through it, but it can still be received.

ch := make(chan int) // Create an integer channel
close(ch) // Close the channel

The <- operator is used to send data to and receive data from channels.

ch <- 42 // Send the integer value 42 to the channel
value := <-ch // Receive data from the channel and store it in the variable value

Both sending and receiving operations on channels are blocking. If a channel is full, a send operation will block until there’s space. Similarly, a receive operation will block if the channel is empty.

Channels have a capacity, which represents the maximum number of elements the channel can hold simultaneously. Channel state includes whether it’s closed and the current number of stored elements.

ch := make(chan int, 5) // Create an integer channel with capacity 5
ch <- 1 // Send data
len(ch) // Return the number of elements in the channel
cap(ch) // Return the channel's capacity

6 Refs