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 twofloat32
components.complex128
: Represents a complex number with twofloat64
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
andstrings.HasSuffix
functions check if a string starts or ends with a specified prefix or suffix. - The
strings.ToLower
andstrings.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 thecap
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 betrue
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 theShape
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 aNew
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’sfmt
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 likefmt.Println
, they automatically invoke the object’sError()
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