README ¶
Table of Contents
- Table of Contents
- The History of Go
- Go - The Programming Language
- Getting Started with Go
- Running Go Applications
- Variables
- Primitives
- Constants
- Arrays and Slices
- Maps
- Structs
- Conditionals
- Loops
- Defer, Panic and Recover
- Pointers
- Functions
- Interfaces
- Goroutines
- Goroutines: Fundamentals - Processes and Threads
- Goroutines: Green Threads
- Goroutines: Threads in Go and Goroutines
- Goroutines: Deep Dive - How Go Multiplexes Goroutines (Green Threads) onto Operating System Threads
- Goroutines: Creation
- Goroutines: Race Conditions when working with Goroutines and Closures
- Goroutines: Synchronization
- Goroutines: Parallelism
- Goroutines: Best Practices
- Channels
- Generics
- Directives
- Standard Library
- References
The History of Go
Go was created at Google by Robert Griesemar, Rob Pike and Ken Thompson. But one of the questions that we need to ask is, "Why create a new programming language at all?".
At the time that Go was designed, there were three languages that were being used at Google - Python, Java and C/C++. However, the Go designers started to recognize that there were some limitations that Google was running into, that might not be able to be fixed, given the history and the designs of the existing programming languages.
For example,
- Python is very easy to use, but it's an interpreted language, hence, slow at Google scale.
- Java is very quick, but its type system has become increasingly complex over time. This is a natural trend a lot of languages go through - they start out simple but as additional use cases become common, additional features are layered on top of the language, it becomes increasingly more difficult to navigate.
- C/C++ is fast but it suffers from a complex type system and additionally its compile times are notoriously slow. The type system however has been receiving a lot of attention lately in the C/C++ communities, however there is still the burden of needing to manage/migrate legacy code. Hence, just like Java, its very difficult to move past the history of what they have, because C++ applications written 10 years ago, still need to be compiled today. The slow compile times are another legacy issue that C/C++ have inherited.
When C/C++ were designed, computers didn't have nearly the memory they have today, so the decision was made to optimize the compilers to use a minimum amount of memory and one of the compromises that brought about was that compile times can be a bit sluggish. In addition, all three languages were created in a time when multi-threaded applications were extremely rare. Almost every application that was created, really focussed on a single thread at a time. So concurrency patterns built into these languages are patched in at best. So working in highly parallel, highly concurrent applications like Google often runs into can be a little bit challenging in these languages.
Enter Go.
Go - The Programming Language
Go is strong and statically typed language similar to Java and C++. Strongly typed means, the type of a variable cannot change over time. Statically typed means, all these variables have to be defined at compile time. Go does have features to go around its type system, but 99% of the time, you are going to be using Go's strong and statically typed environment.
Key features:
- Strong and Statically Typed
- Excellent Community
- Key principles
- Simplicity
- Extremely fast compile times
- Garbage collection baked in
- Built-in concurrency in the base language.
- Compile to standalone binaries. Which means when you compile your Go application, everything is going to be bundled into that single binary that's related to the Go application itself. So, the Go runtime, any Go dependencies, all get bundled in. There is thus, no need to reach out to external libraries, DLLs and other things like that to make your application work. The benefit is that it makes version management at runtime becomes trivial.
Getting Started with Go
Installation
-
Installation:
Option 1: Go can be installed via Homebrew for Linux and Mac by running:
# Using Homebrew brew install go
Option 2: Download a Go binary release from the Downloads Page
-
Download VS Code Extension: Download the Go Extension maintained by the Go Team at Google to add language support for IDE intellisense and auto-completion.
Launch VS Code Quick Open (
Ctrl + P
), paste the following command, and pressenter
.ext install golang.Go
-
Download the additional packages required by the extension: To make the extension function properly there are some additional packages that must be downloaded.
# go-outline is an utility to extract JSON representation of declarations from a Go source file # gopls is the official Language Server for Go # go-delve is a debugger for Go # staticcheck offers extensive analysis of Go code, detecting bugs, dead code, pointing suggestions go install -v github.com/ramya-rao-a/go-outline@latest go install -v golang.org/x/tools/gopls@latest go install github.com/go-delve/delve/cmd/dlv@latest go install honnef.co/go/tools/cmd/staticcheck@latest
-
Edit
settings.json
in VS Code and add the following:{ "[go]": { "editor.defaultFormatter": "golang.go", "editor.formatOnSave": true }, "go.useLanguageServer": true, "go.languageServerFlags": ["-rpc.trace"], "gopls": { "verboseOutput": true }, "files.associations": { "*.gohtml": "html", "*.tmpl": "html" }, // If VSCode is not able to find Go, you can provide the path explicitly like so "go.alternateTools": { "go": "/opt/homebrew/bin/go" } }
This sets up debugging options and uses the
gopls
Language Server. This will fetch imports automatically if they are not imported and add other formatting.
Workspace
If you're using Go prior to Go 1.11
, it was preferred to create a workspace inside a src
folder inside the GOPATH
. The GOPATH
is the path where Go installs packages, binaries and reads source code from. The basic structure looks like this:
├── bin
├── pkg
├── README.md
└── src
└── github.com
└── jayantasamaddar
└── firstapp
└── Main.go
So any project you create should go inside the src
folder.
But ever since Go modules came out, this way of following the GOPATH
structure is no more applicable when using Go modules and is the current defacto way of working with Go. However, it is still important to know the significance of the structure of the GOPATH
and how Go stores installable binaries and packages.
Just remember, whenever you run go install
, by default, it will store in the $GOPATH/bin
directory.
Modules
Single Module Workspace
A module can be considered as a single project which can generate a binary of its own.
Go understands a project is a module when we add the go.mod
file.
We can initialize a module by typing the following to create a go.mod
file:
go mod init github.com/[github-username]/[publicrepository]
A basic go.mod
file looks like this without dependencies.
module "github.com/jayantasamaddar/publicrepository"
go 1.20
As you add dependencies in your projects, you may run:
# Run this when you import an external package in any of the files in the module that hasn't been imported to auto-import them into your project.
go mod tidy
The go.mod
file will be updated to require those dependencies like:
module github.com/jayantasamaddar/quick-reference-golang/cli-reminder
go 1.20
require (
github.com/gen2brain/beeep v0.0.0-20230307103607-6e717729cb4f
github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8
)
The go.sum
will be created tracking all the dependencies versions with checksum.
Rules and Conventions:
- The naming convention when declaring a module is to name it according to the repository from which it would be downloaded.
- A directory containing a module needs to have a file with a
main
function that is run as the entrypoint to the module. - An
init
function(s) if it exists (there can be more than one), runs once prior to themain
function. If there are more than oneinit
functions located in separate files, they are run in an alphabetical order. This isn't ideal and should be avoided. If you provide aninit
function, provide only one.
Multi-Module Workspace
Multi-Module Workspace: Basic Setup
A multi-module workspace can be seen very similar to a Monorepo where there are many Go modules inside a single Directory or Workspace. This repository quick-reference-golang
is an example of a Multi-Module Workspace.
It is characterized by Directories which are modules. For example the basic-CRUD-api
, cli-reminder
are modules of the quick-reference-golang
workspace. These modules are setup exactly as described in the Single-Module Workspace section.
The only thing to add now is a go.work
file that includes those modules in this root directory.
go 1.20
use (
./basic-CRUD-api
./cli-reminder
)
This can also be initialized with the go work
command.
Note: The modules must be created first, i.e. they must contain a
go.mod
file inside each of them, before you add them to thego.work
Relevant Commands:
go work init [modulePath]
: Initialize workspace file with a module.go work use [modulePath]
: Add another module to the workspace.
go work init ./basic-CRUD-api
go work use ./cli-reminder
Now the modules are properly synced with the workspace.
Multi-Module Workspace: Dependent Modules
The whole point of a Multi-Module workspace is that modules should be able to depend on each other without fetching them from external repositories.
Situation: You have a workspace, with three modules:
moduleA
moduleB
moduleC
moduleB
has no dependencies while moduleA
and moduleC
both depend on moduleB
.
We don't want our import statement to look on the internet or try to fetch from a repository moduleB
. Afterall, that is why we are in a Multi-Module setup. That is especially true, when we have just created a new multi-module workspace and moduleB
doesn't even exist in any repository because there has been no commits pushed. On top of that, any new changes to moduleB
may not exist in that external repository yet, hence we will be accessing stale code and that may cause bugs in moduleA
and moduleC
.
To fix this, we need to simply replace the moduleB
with the path to moduleB in the workspace in each of the go.mod
file of moduleA
and moduleC
.
To do this,
-
cd
into themoduleA
directory. -
Run:
go mod edit -replace github.com/user/moduleB=../moduleB
This will modify the
go.mod
file to have areplace
statement added, to look something like:module github.com/user/moduleA go 1.20 require ( // ...other dependencies github.com/user/moduleB v0.0.0-00010101000000-000000000000 ) replace github.com/user/moduleB => ../moduleB
-
Repeat the above for
moduleC
.
Now, we are correctly setup in a Multi-Module Workspace that have inter-dependent modules.
Running Go Applications
Running your first Go Application
A small Hello World application in Go:
package main
import "fmt"
func main() {
fmt.Printf("hello, world\n")
}
Running the Go application can be done in three ways:
go run [path]
go build [modulePath]
and then running the executable./name
. With the environment variableCGO_ENABLED=0
you get a statically-linked binary (see: Static build) so it will run without any external dependencies (you can buld your docker imagefrom SCRATCH
)go install [moduePath]
and then accessing it via the bin directory likeGOPATH/bin/name
Live Reloading Go Applications
Live Reloading Go Applications can be done using any one of the following methods:
-
Using
Nodemon
: Those coming from a JavaScript background can use:nodemon --exec go run [filename.go] --signal SIGTERM
Nodemon also works for any executable file with the
--exec
flag.Note: Nodemon needs to be installed globally using npm:
npm i -g nodemon
Variables
Variables: Declaration
Here are the rules when it comes to variables:
- Every variable in Go has an initialization value.
Variables can be declared in the following ways:
package main
import "fmt"
func main() {
/** (1) Initializing a variable with a type and then assigning.
* Useful when not ready to use a variable but can declare it.
*/
var i int
i = 40
/** (2) Initializing a variable with a type and immediately assigning it.
* Useful when go doesn't have enough information to infer a type and assign it.
*/
var j float32 = 50
/** (3) Initializing a variable and letting the Go compiler infer type.
* Simple, but there is no explicit way to set the type.
* Cannot be used to declare variables at the package level.
*/
k:= 60
l:= 70.
/** Variable blocks */
var (
first_name string = "Jayanta"
last_name string = "Samaddar"
age int = 31
)
/** Using the fmt module. Check: https://pkg.golang.ir/fmt for usage */
fmt.Println(i) // 40
fmt.Printf("%v, %T\n", j, j) // 50, float32
fmt.Printf("%v, %T\n", k, k) // 60, int
fmt.Printf("%v, %T\n", l, l) // 70, float64 | Go infers the type as float64 and not float32
fmt.Printf("%[2]v %[1]v, %[3]d yrs", first_name, last_name, age) // Samaddar Jayanta, 31 yrs
}
Variables: Redeclaration and Shadowing
When it comes to redeclaration, these are the rules to remember:
- Variables with the same name declared in the same scope cannot be redeclared.
- Variables may however, be reassigned but to the same type which was declared.
- Variables with the same name maybe declared more than once in different scopes - for e.g. once in the package scope and once within the function scope. In this case, the variable with the innermost scope takes precedence.
- Variables always have to be used. Go throws a declared and not used compile time error to keep the program clean.
package main
import "fmt"
var i int = 20
func main() {
fmt.Println(i) // 20
var i int = 30
fmt.Println(i) // 30
// i:= 50 // throws an error
i = 40
fmt.Println(i) // 40
}
Variables: Visibility
There are three levels of visibility of variables in Go.
-
Package Scope: If a variable is declared in lowercase at the package level, it is scoped to the package and any file in the same package can access it.
-
Global Scope: If a variable is declared in uppercase at the package, it is exported from the package and is globally visible.
-
Block Scope: When a variable is declared within a function block inside one of the files, that variable is scoped to that block and is not visible outside the block.
Variables: Naming Rules and Conventions
Rules:
- Lowercased variables at the package level are package scoped.
- Uppercased variables at the package level are exported to the global scope.
Conventions (Not hard rules):
- Follow
camelCase
orPascalCase
naming convention. - Variable names that contain acronyms like
URL
should be uppercased. - The length of the variable name should ideally reflect the life of the variable.
Variables: Type Conversions
Go doesn't do implicit type conversion found in interpreted languages like Python or JavaScript.
Instead, in Go, we use the expression T(v)
to convert the value v
to the type T
.
package main
import "fmt"
func main() {
var i int = 40
fmt.Printf("%v, %T\n", i, i) // 40, int
var j float32 = float32(i)
fmt.Printf("%v, %T\n", j, j) // 40, float32
var k float32 = 50.5
fmt.Printf("%v, %T\n", i, i) // 50.5, float32
var l int = int(k)
fmt.Printf("%v, %T\n", l, l) // 50, int
}
While this works well for number data types, when it comes to strings, it works differently.
package main
import "fmt"
func main() {
var i int = 65
fmt.Printf("%v, %T\n", i, i) // 65, int
var j string = string(i)
fmt.Printf("%v, %T\n", j, j) // A, string
}
A string is just an alias for a stream of bytes. So what happens when we asked the function to convert the integer 65
into a string, it looks for what ASCII character is set to the value 65
and that happens to be the character A
(capital).
To convert the number 65
into a string version of it, i.e. "65"
, we need to use the strconv
package and use its Itoa
method.
package main
import (
"fmt"
"strconv"
)
func main() {
var i int = 65
fmt.Printf("%v, %T\n", i, i) // 65, int
var j string = strconv.Itoa(i) // ItoA = I(integer) to A(ASCII string)
fmt.Printf("%v, %T\n", j, j) // 65, string
k, err := strconv.Atoi(j) // Atoi = A(ASCII string) to i(Integer)
fmt.Printf("%v, %T\n", k, k) // 65, int
fmt.Printf("%v, %T\n", err, err) // <nil>, <nil>
/**
* The parse functions return the widest type (float64, int64, and uint64),
* but if the size argument specifies a narrower width the result can be converted
* to that narrower type without data loss:
*/
l, err := strconv.ParseInt(j, 10, 64)
m:= int(l)
fmt.Printf("%v, %T\n", l, l) // 65, int64
fmt.Printf("%v, %T\n", err, err) // <nil>, <nil>
fmt.Printf("%v, %T\n", m, m) // 65, int
}
Primitives
Boolean
- A Boolean variable declared but not assigned will have its default value to be 0, i.e. false.
- A Boolean type is returned as the result of a logical test.
package main
import (
"fmt"
)
func main() {
var a bool
var b bool = true
fmt.Printf("%v, %T\n", a, a) // false, bool
fmt.Printf("%v, %T\n", b, b) // true, bool
/** A boolean is returned as the result of a Logical Test */
x := 30
y := 40
gt := x > y
lt := x < y
eq := x == y
fmt.Printf("%v, %T\n", gt, gt) // false, bool
fmt.Printf("%v, %T\n", lt, lt) // true, bool
fmt.Printf("%v, %T\n", eq, eq) // false, bool
}
Numeric Types
Integers
Integers: Types
The different types of Integers we can work with are:
- Signed Integers
int
: The general integer type that is of minimum 32 bits regardless of environment but can stretch to a maximum of 128 bits depending on the system you are running on.int8
: Integers between-128
to127
int16
: Integers between-32,768
to32,767
int32
: Integers between-2,147,483,648
to2,147,483,647
int64
: Integers between-9,223,372,036,854,775,808
to-9,223,372,036,854,775,807
- Unsigned Integers
Unsigned Integers (often called uints
) are just like integers (whole numbers) but have the property that they don't have a +
or -
sign associated with them. Thus they are always non-negative (zero or positive). We use uints
when we know the value we are counting will always be non-negative. For example, if we are counting the number of players in a game, we could use a uint because there will always be 0 or more players.
Note: it is almost always the case that you could use a regular integer variable in place of an unsigned integer. The advantage to using the unsigned version (when you know the values contained will be non-negative) is that sometimes the computer will spot errors for you (the program will "crash" when a negative value is assigned to the variable).
uint
:0
to255
uint8
:0
to255
byte
:0
to255
(alias foruint8
)uint16
:0
to65,536
uint32
:0
to4,294,967,295
Integers: Arithmetic Operations
- Integer Operations always have to be done with the same integer type.
- We can do all arithmetic operations with Integers of the same type. However an integer only operation returns an integer type.
package main
import "fmt"
func main() {
a:= 10
b:= 3
fmt.Println(a + b) // 13
fmt.Println(a - b) // 7
fmt.Println(a * b) // 30
fmt.Println(a / b) // 3 => An integer divided by an integer returns an integer
fmt.Println(a % b) // 1 => Remainder
}
As you can see, the integer division did not give us a result where there are decimals as 10
divided by 3
is 3.3333333
but since we do an integer division, the remainder is dropped.
Note: To get the decimal we can simply do a type conversion to
float32
for each value before we do the operation.
package main
import "fmt"
func main() {
a:= 10
b:= 3
fmt.Println(float32(a) / float32(b)) // 3.3333333
}
Integers: Bitwise Operators
Compares bits of the first number to the bits of the second number as per the operation.
package main
import "fmt"
func main() {
a:= 10 // Binary: 1010
b:= 3 // Binary: 0011
// Both have the bit set
fmt.Println(a & b) // 2 => Binary: 0010 = 2
// Either one has the bit set
fmt.Println(a | b) // 11 => Binary: 1011 = 11
// One or the other have the bit set, but not both
fmt.Println(a ^ b) // 9 => Binary: 1001 = 9
// Neither have the bit set
fmt.Println(a &^ b) // 8 => 0100 = 8
}
Integers: Bitshift Operators
package main
import "fmt"
func main() {
a:= 8 // 2^3
// Bitshifts left 3 places
fmt.Println(a << 3) // 2^3 * 2^3 = 2^6 = 64
// Either one has the bit set
fmt.Println(a >> 3) // 2^3 / 2^3 = 2^0 = 1
}
Floats
In Go, Floating Point Numbers or Floats follow IEEE-754 Standard. We can use either decimal numbers or exponential notation.
Floats: Types
We have the following types:
float32
: 32-bit Floating Point Numbersfloat64
: 64-bit Floating Point Numbers
/** Initializing Floating Point Numbers */
package main
import "fmt"
func main() {
a:= 3.14
var b float64 = 13.7e72
var c float32 = 2.1E14
fmt.Printf("%v, %T\n", a, a) // 3.14, float64 (float64 is default when initializer syntax)
fmt.Printf("%v, %T\n", b, b) // 1.37e+73, float64
fmt.Printf("%v, %T\n", c, c) // 2.1e+14, float32
}
Floats: Arithmetic Operations
- Floating Point Operations always have to be done with the same floating point type. (i.e. float32 type cannot be added to a float64 type. Type conversion must be done prior to that.)
- We can do all arithmetic operations with Floating Point numbers of the same type. The return type is always a floating point number type.
- The Modulo operator (
%
), and Bitwise and Bitshift operators are not available. They are only available to Integer types.
package main
import "fmt"
func main() {
a:= 10.2
b:= 3.7
fmt.Println(a + b) // 13.899999999999999
fmt.Println(a - b) // 6.499999999999999
fmt.Println(a * b) // 37.74
fmt.Println(a / b) // 2.7567567567567566
}
Complex Numbers
It is rare for programming languages to treat complex numbers as a first class citizen. This opens Go to be used as a powerful language for Data Science.
Complex Numbers: Types
There are two types of Complex Numbers in Go.
complex64
: Takes afloat32
+float32
for the real and imaginary partscomplex128
: Takes afloat64
+float64
for the real and imaginary parts
Note:
- We can use wrap a complex number in
real()
orimaginary()
to get the real and imaginary parts out respectively.- Similarly we can take two numbers (representing the real and imaginary part respectively) and convert them into a complex number using the
complex()
function that takes two numbers as arguments.- Go's parser understands the
i
as an imaginary number.
package main
import "fmt"
func main() {
var a complex64 = 1 + 2i
var b complex64 = 2i
fmt.Printf("%v, %T\n", a, a) // (1+2i), complex64
fmt.Printf("%v, %T\n", b, b) // (0+2i), complex64
fmt.Printf("%v, %T\n", real(a), real(a)) // 1, float32
fmt.Printf("%v, %T\n", imag(b), imag(b)) // 2, float32
fmt.Println(complex(1, 2)) // (1+2i)
}
Complex Numbers: Arithmetic Operations
package main
import "fmt"
func main() {
var a complex64 = 1 + 2i
var b complex64 = 2 + 5.2i
fmt.Println(a + b) // (3+7.2i)
fmt.Println(a - b) // (-1-3.1999998i)
fmt.Println(a * b) // (-8.4+9.2i)
fmt.Println(a / b) // (0.39948454-0.03865979i)
}
Text Types
Strings
Strings: Overview
In Go,
- A String can be any UTF-8 character.
- We declare strings with double-quotes.
"This is a string"
- Strings in Go are aliases for bytes
- Strings are immutable.
package main
import "fmt"
func main() {
s:= "This is a string"
/** Get the third character of the string */
s2:= s[2]
// s[2] = "L" // Unassignable operand. Immutable.
fmt.Printf("%v, %T\n", s, s) // This is a string, string
fmt.Printf("%v, %T\n", s2, s2) // 105, uint8
fmt.Printf("%v, %T\n", string(s2), string(s2)) // i, string
}
Strings: Operations
package main
import "fmt"
func main() {
s:= "This is a string"
/** Concatenating Strings */
fmt.Println(s + ". " + "Okay!") // This is a string. Okay!
fmt.Printf("%v. Okay!\n", s) // This is a string. Okay!
/** Convert a string into a collection of bytes */
b:= []byte(s)
fmt.Printf("%v, %T\n", b, b) // [84 104 105 115 32 105 115 32 97 32 115 116 114 105 110 103], []uint8
}
Important: A lot of the functions that we use in Go, work with byte slices. That makes them much more generic and much more flexible, than if we worked with hard coded strings. For example, if you want to send a string response to a web request, you can easily convert it to a collection of bytes. If you want to send a file back, even a file is just a collection of bytes too. Thus it allows transparency to work with strings without worrying about line endings and things like that. So while, we might be working with strings in our Go programs as strings, when we start sending them to other applications, we may just be sending them as byte slices.
Go is a modular language, i.e. unlike many languages, for a lot of the functionality, we have to use packages we have to import and use. Go ships with the strings
package, which we can import and use for additional functions to deal with strings.
Runes
Runes: Overview
In Go,
- A Rune is UTF-32 character.
- We declare runes with single-quotes.
'This is a rune'
- Runes in Go are aliases for
int32
. - While strings can be converted back and forth between collection of bytes and string literal, runes in Go are a true type alias of
int32
. It is the same as aint32
.
Note: While a String type represents any UTF-8 character, a rune represents any UTF-32 character. UTF-32 is a little weird as while any character in UTF-32 can be 32-bits long, it doesn't have to be 32-bits long. Any UTF-8 character which is 8-bits long, is also a UTF-32 character.
package main
import "fmt"
func main() {
r:= 'r'
fmt.Printf("%v, %T\n", r, r) // 114, int32
}
Note: Runes are useful when working with UTF-32 characters. Read more about runes in the ReadRune API documentation in the Go
strings
package.
Constants
- All constants are declared with the
const
keyword. - Has to be assigned at compile time and not runtime. So assigning a function return value to a constant is going to throw an error.
- Immutable and cannot be reassigned. Thus arrays which are mutable cannot be constants.
- If we come from other programming languages, we may think of declaring constants with all uppercase letters like:
const MY_CONST
. However with Go, as discussed earlier, all uppercase indicates global scope, hence we declare constant variable names just how we declare normal variables: either in camelCase if we didn't want to globally export it or PascalCase if we wanted to export it globally. - Can be shadowed.
- Can run operations with constants or variables of the same type. The return value is a variable.
- Doesn't throw an error if unused unlike variables.
Constants: Typed Constants
Typed constants are declared just like typed variables but with the const
keyword instead of the var
keyword. Typed constants offer type safety that restricts interoperability only to the same type unless there is a type conversion done.
package main
import "fmt"
const a int32 = 10
const b float32 = 10
const c = a + int32(b) // Type conversion of the float32 type to int32
// const d = math.Sin(math.Pi/4) // (value of type float64) is not a constant
const e int8 = 255
// const x // missing init expr for x
func main() {
var v int32 = 30
fmt.Printf("%v, %T\n", a, a) // 10, int32
fmt.Printf("%v, %T\n", b, b) // 10, float32
// fmt.Println(a + b) // invalid operation:(mismatched types int32 and float32)
fmt.Printf("%v, %T\n", sum, sum) // 20, int32
fmt.Printf("%v, %T\n", e, e) // 255, uint8
const e uint8 = 128 // (Can be shadowed)
fmt.Printf("%v, %T\n", e, e) // 128, uint8
fmt.Printf("%v, %T\n", a+v, a+v) // 40, int32 (Valid operation using const and var)
}
Constants: Untyped Constants
Untyped constants refer to constants whose type is determined by the Go compiler and not specified at declaration. Constants are replaced by their value at runtime by the Go compiler hence untyped constants can interoperate with similar types.
package main
import "fmt"
const a = 10
var v int16 = 35
func main() {
/** Constants are replaced by their values at runtime: e.g. (10 + v) in this case */
fmt.Printf("%v, %T\n", a + v, a + v) // 45, int16
fmt.Printf("%v, %T\n", 10 + v, 10 + v) // 45, int16 (Same as above)
}
Constants: Enumerated Constants
In Go, enumerated constants are created using the iota
enumerator. Since iota
can be part of an expression and expressions can be implicitly repeated, it is easy to build intricate sets of values. E.g. we can use iota
as a counter.
Here are some other characteristics:
- The value of
iota
is block scoped. - Initial value of
iota
is0
. - If we do not assign the value
iota
, the compiler is going to try to infer the value of the pattern.
package main
import "fmt"
const i = iota
const (
a = iota
b = iota
c = iota
d
e
)
const (
x = iota + 10 // All integer operations are permitted
y
z
)
func main() {
const j int = y
fmt.Printf("%v, %T\n", i, i) // 0, int
/** Using `iota` as a counter */
fmt.Println(a) // 0
fmt.Println(b) // 1
fmt.Println(c) // 2
fmt.Println(d) // 3
fmt.Println(e) // 4
fmt.Println(x, y, z) // 10 11 12
fmt.Println(j == y) // true
}
Note: Often a situation requires you to compare an iota value to an integer. However, an unassigned integer value is also
0
. To avoid this situation, we can determine the first iota value to be an error value.Assigning a
_
name to the0
value of the enumerated constant assigns it to a blank identifier
package main
import "fmt"
const (
_ = iota // ignore first value by assigning to blank identifier
x
y
z
)
func main() {
var j int
fmt.Println(x, y, z) // 1 2 3
fmt.Println(j == x) // false
}
Constants: Enumeration Expressions
Bitshifting to enumerate to the power
Bitshifting enumerated constants, is one of the ways to use Enumerated constants. The reason in because we don't have the ability to raise to power of an enumerated constant, as raising to the power in Go, is a function in the math
package, so we cannot do that in our constant enumeration. We can overcome this by using Bitshift operators.
package main
import "fmt"
const (
_ = iota // ignore first value by assigning to blank identifier
KB = 1 << (10 * iota) // 2^0 * 2^(10 * 1) = 1024
MB // 2^0 * 2^(10 * 2) = 1048576
GB // 2^0 * 2^(10 * 3) = 1073741824
TB // 2^0 * 2^(10 * 4) = 1099511627776
PB // 2^0 * 2^(10 * 5) = 1125899906842624
EB // 2^0 * 2^(10 * 5) = 1152921504606846976
)
func main() {
fileSize := 4000000000.
fmt.Printf("%.2fGB\n", fileSize/GB) // 3.73GB
fmt.Println(KB) // 1024
fmt.Println(MB) // 1048576
fmt.Println(GB) // 1073741824
fmt.Println(TB) // 1099511627776
fmt.Println(PB) // 1125899906842624
fmt.Println(EB) // 1152921504606846976
}
Bitshifting using Boolean flags
We can use Bitshifting to set boolean flags inside of a single byte. This makes memory efficient code. In the code below, we can store 8 access roles for users into a single byte of data.
package main
import "fmt"
const (
isAdmin = 1 << iota // 2^0 * 2^0 = 1
isHeadquarters // 2^0 * 2^1 = 2
canSeeFinancials // 2^0 * 2^2 = 4
canSeeAfrica // 2^0 * 2^3 = 8
canSeeAsia // 2^0 * 2^4 = 16
canSeeEurope // 2^0 * 2^5 = 32
canSeeNorthAmerica // 2^0 * 2^6 = 64
canSeeSouthAmerica // 2^0 * 2^7 = 128
)
func main() {
/**
* isAdmin = 00000001
* canSeeFinancials = 00000100
* canSeeEurope = 00100000
* isAdmin | canSeeFinancials | canSeeEurope = 00100101
*/
var roles byte = isAdmin | canSeeFinancials | canSeeEurope
fmt.Printf("%b\n", roles) // 100101
fmt.Printf("isAdmin: %v\n", isAdmin & roles == isAdmin) // isAdmin: true
fmt.Printf("canSeeAsia: %v\n", canSeeAsia & roles == canSeeAsia) // canSeeAsia: false
}
Arrays and Slices
Arrays and Slices are two collection types available in Go.
Arrays: Creation
With Arrays, by the design of the language, the elements are contiguous in memory. So collecting our data together in arrays, not only is it easier to work with, but also makes our applications fast.
In Go, unlike C
- Arrays are values. Assigning one array to another copies all the elements.
- In particular, if you pass an array to a function, it will receive a copy of the array, not a pointer to it.
- The size of an array is part of its type. The types [10]int and [20]int are distinct.
package main
import "fmt"
func main() {
var empty [3]string
nums := [3]int{100, 200, 300}
// The dots mean, create an array just long enough to hold the elements passed
scores := [...]int{100, 200, 300, 400, 500}
fmt.Printf("%v, %T\n", empty, empty) // [ ], [3]string
fmt.Printf("%v, %T\n", nums, nums) // [100 200 300], [3]int
fmt.Printf("%v, %T\n", scores, scores) // [100 200 300 400 500], [5]int
/** Assigning to arrays: Assigns the value to index 1. Indexes start at 0. */
scores[1] = 150
fmt.Printf("%v, %T\n", scores, scores) // [100 150 300 400 500], [5]int
}
Arrays: Built-in Functions
package main
import "fmt"
func main() {
scores := [...]int{100, 200, 300, 400, 500}
fmt.Printf("%v, %T\n", scores, scores) // [100 200 300 400 500], [5]int
/** Get the length of the array */
fmt.Println(len(scores)) // 5
}
Note: Use the index
len(a)-1
to access the last element of a slice or arraya
. Go doesn't have negative indexing like Python does.
Arrays: Working with Arrays
Here are some other ways we can work with arrays:
package main
import "fmt"
func main() {
/** A 2-dimensional array */
var matrix = [3][3]int{ {1, 0, 0}, {0, 1, 0}, {0, 0, 1} }
fmt.Printf("%v, %T\n", matrix, matrix) // [[1 0 0] [0 1 0] [0 0 1]], [3][3]int
/** Array passed as a new copy */
a := [...]int{1, 2, 3}
b := a
b[2] = 4
fmt.Printf("a: %v\n", a) // a: [1 2 3]
fmt.Printf("b: %v\n", b) // b: [1 2 4] => Original array is not mutated
/** Array passed as a pointer */
c := [...]int{1, 2, 3}
d := &c
d[2] = 5
fmt.Printf("c: %v\n", c) // c: [1 2 5]
fmt.Printf("d: %v\n", d) // d: &[1 2 5] => Original array is mutated
}
Arrays are very useful and there are use cases for arrays. However the fact that their size is limited and must be known at compile time, limits their usefulness. In Go, the most common use case for an array is to back a Slice.
Slices: Creation
Slices wrap arrays to give a more general, powerful, and convenient interface to sequences of data. Except for items with explicit dimension such as transformation matrices, most array programming in Go is done with slices rather than simple arrays.
There are three ways to create a Slice:
- Using the empty square brackets to denote a slice at initialization
- Using the
:
syntax - Using a built-in-function:
make
package main
import "fmt"
func main() {
// A slice is initialized with the empty square brackets
var empty []string
nums := []int{100, 200, 300}
s := []int{100, 200, 300, 400, 500}
fmt.Printf("%v, %T\n", empty, empty) // [], []string
fmt.Printf("%v, %T\n", nums, nums) // [100 200 300], []int
fmt.Printf("%v, %T\n", s, s) // [100 200 300 400 500], []int
/** Assigning to slices: Assigns the value to index 1. Indexes start at 0. */
s[1] = 150
fmt.Printf("%v, %T\n", s, s) // [100 150 300 400 500], []int
/**
* Another way of creating a slice following the syntax: s[indexIncluding:indexExcluding]
*/
s1 := s[:] // slice of all elements
s2 := s[3:] // slice from 4th element to end
s3 := s[:4] // slice first 5 elements
s4 := s[3:5] // slice the 4th, 5th
s5 := s[1:len(s) - 1] // slice all elements leaving the first and last
fmt.Println(s1) // [100 150 300 400 500]
fmt.Println(s2) // [400 500]
fmt.Println(s3) // [100 150 300 400]
fmt.Println(s4) // [400 500]
fmt.Println(s5) // [150 300 400]
}
Slices: Built-in Functions
package main
import "fmt"
func main() {
scores := []int{100, 200, 300, 400, 500}
fmt.Printf("%v, %T\n", scores, scores) // [100 200 300 400 500], []int
/** Get the length of the slice */
fmt.Println(len(scores)) // 5
/** Get the capacity of the slice which is the length of the underlying array.
* The number of elements of the slice doesn't necessarily match the size of the
* array backing it. Capacity refers to the length of the underlying array.
*/
fmt.Println(cap(scores)) // 5 => Underlying array is same size
/** Push a new element to the end of a slice */
scores = append(scores, 600)
fmt.Printf("%v, %T\n", scores, scores) // [100 200 300 400 500 600], []int
fmt.Printf("Len: %v, Cap: %v\n", len(scores), cap(scores)) // Len: 6, Cap: 10
/** Slice whose capacity remains same as its length */
var names []string
fmt.Printf("Len: %v, Cap: %v\n", len(names), cap(names)) // Len: 0, Cap: 0
names = append(names, "Tom")
fmt.Printf("Len: %v, Cap: %v\n", len(names), cap(names)) // Len: 1, Cap: 1
names = append(names, "Jerry", "Johnny", "Bravo")
fmt.Printf("Len: %v, Cap: %v\n", len(names), cap(names)) // Len: 4, Cap: 4
}
Slices: Working with Slices
- Slices hold references to an underlying array, and if you assign one slice to another, both refer to the same array.
- If a function takes a slice argument, changes it makes to the elements of the slice will be visible to the caller, analogous to passing a pointer to the underlying array.
- A Read function can therefore accept a slice argument rather than a pointer and a count; the length within the slice sets an upper limit of how much data to read.
- Use the index
len(a)-1
to access the last element of a slice or arraya
. Go doesn't have negative indexing like Python does. - One has to be careful when mutating slices as the underlying array will be mutated too as slices hold reference not value.
package main
import "fmt"
func main() {
/**
* Creating a slice using `make`:
* Syntax: make(type, length, capacity)
*/
a := make([]int, 3)
fmt.Println(a) // [0 0 0]
fmt.Printf("Len: %d, Cap: %d\n", len(a), cap(a)) // Len: 3, Cap: 3
b := make([]int, 3, 6)
fmt.Println(b) // [0 0 0]
fmt.Printf("Len: %d, Cap: %d\n", len(b), cap(b)) // Len: 3, Cap: 6
/** Concatenating slices */
x := []int{1, 2, 3, 4, 5}
y := []int{11, 12, 13, 14, 15}
z := append(x, y...)
fmt.Println(z) // [1 2 3 4 5 11 12 13 14 15]
/** Treating the slice as a stack */
x = x[1:] // Shift operation (remove first element)
fmt.Println(x) // [2 3 4 5]
x = x[:len(x) - 1] // Pop operation (remove last element)
fmt.Println(x) // [2 3 4]
/** Removing elements from the middle */
x = []int{1, 2, 3, 4, 5}
x = append(x[:1], x[len(x) - 2:]... ) // Remove elements between first and second last
fmt.Println(x) // [1 4 5]
}
Maps
Maps: Overview
Maps are a convenient and powerful built-in data structure that associate values of one type (the key) with values of another type (the element or value). They can be created via literals or via make
function.
Here are some rules when it comes to Maps
- The key can be of any type for which the equality operator is defined, such as integers, floating point and complex numbers, strings, pointers, interfaces (as long as the dynamic type supports equality), structs and arrays.
- Slices and other maps cannot be used as map keys, because equality is not defined on them.
- Like slices, maps hold references to an underlying data structure. If you pass a map to a function that changes the contents of the map, the changes will be visible in the caller.
- The return order of a Map is not guaranteed.
Maps: Creation
Within a single map, there can be multiple keys of one Key type that can be mapped to their respectively values of only one value type.
package main
import "fmt"
func main() {
// Using map literal
var timeZone = map[string]int{
"UTC": 0*60*60,
"EST": -5*60*60,
"CST": -6*60*60,
"MST": -7*60*60,
"PST": -8*60*60,
}
fmt.Println(timeZone) // map[CST:-21600 EST:-18000 MST:-25200 PST:-28800 UTC:0]
// Using make
groceries := make(map[string]float32)
groceries["Milk"] = 2.30
groceries["Fish"] = 15.25
groceries["Eggs"] = 1.99
fmt.Println(groceries) // map[Eggs:1.99 Fish:15.25 Milk:2.3]
}
Maps: Manipulation
package main
import "fmt"
func main() {
timeZone := map[string]int{
"UTC": 0*60*60,
"EST": -5*60*60,
"CST": -6*60*60,
"MST": -7*60*60,
"PST": -8*60*60,
}
fmt.Println(timeZone) // map[CST:-21600 EST:-18000 MST:-25200 PST:-28800 UTC:0]
/** Accessing the value of a key */
fmt.Printf("UTC: %v\n", timeZone["UTC"]) // UTC: 0
/** Adding a new key value */
timeZone["IST"] = 5*60*60 + 0.5*60*60
fmt.Println(timeZone) // map[CST:-21600 EST:-18000 IST:19800 MST:-25200 PST:-28800 UTC:0]
/** Delete a map entry. Safe to use even if key is absent. */
delete(timeZone, "MST")
fmt.Println(timeZone) // map[CST:-21600 EST:-18000 IST:19800 PST:-28800 UTC:0]
/** "comma ok" idiom to check if a value exists */
pop := timeZone["GST"]
fmt.Println(pop) // 0 (If not found, always returns 0)
// Non-existing key returns ok as `false`
key, ok := timeZone["GST"]
fmt.Println(key, ok) // 0 false
// Existing key returns ok as `true`
ist, ok := timeZone["IST"]
fmt.Println(ist, ok) // 19800 true
/** Check the number of key-value pairs */
fmt.Println(len(timeZone)) // 5
/** Test for presence in the map without worrying about the actual value */
_, present := timeZone["GST"]
}
Structs
Structs: Creation
- Structs can be declared either as a type or as an anonymous struct.
- Can leave out optional fields.
- Structs are stored as values and not references unlike Maps.
Declare a struct as a type:
package main
import "fmt"
type Person struct {
id int
name string
friends []string
}
func main() {
person := Person {
id: 1,
name: "Jayanta",
friends: []string {
"Adraha",
"Rohit",
"Ravi",
},
}
fmt.Println(person) // {1 Jayanta [Adraha Rohit Ravi]}
fmt.Println(person.friends[1]) // Rohit
}
Declare an anonymous struct:
package main
import "fmt"
func main() {
person := struct {
id int
name string
friends []string
}{
id: 1,
name: "Jayanta",
friends: []string{
"Adraha",
"Rohit",
"Ravi",
},
}
fmt.Println(person) // {1 Jayanta [Adraha Rohit Ravi]}
fmt.Println(person.friends[1]) // Rohit
}
Structs as references:
package main
import "fmt"
func main() {
person := struct {
id int
name string
friends []string
}{
id: 1,
name: "Jayanta",
friends: []string{
"Adraha",
"Rohit",
"Ravi",
},
}
fmt.Println(person) // {1 Jayanta [Adraha Rohit Ravi]}
person2 := person; // copies the struct into a new struct
person3 := &person; // points the person struct to person3 struct
person2.name = "Bhargav"
person3.name = "Prince"
fmt.Println(person) // {1 Prince [Adraha Rohit Ravi]}
fmt.Println(person2) // {1 Bhargav [Adraha Rohit Ravi]}
fmt.Println(person3) // &{1 Prince [Adraha Rohit Ravi]}
}
Structs: Naming Convention
The naming conventions are same as variables in Go. If a struct field starts with an uppercase letter, it will be exported to be globally available or else it will be internal to the package.
Structs: Embedding
Go does not provide the typical, type-driven notion of subclassing, i.e. inheritance model, but it does have the ability to "borrow" pieces of an implementation by embedding types within a struct or interface using a composition model called embedding.
- In a composition relationship a struct that embeds another struct, is still its own type.
- When we are talking of modelling behaviour, embedding is not the right choice. Embedding allows methods to carry through to the type that has the embedding. However the fact that we cannot use them interchangeably is a severe limitation. Generally, it's much better to use interfaces, when we are to describe common behaviour (have common methods).
- Embedding is a good idea, when we just want to get some basic behaviour into a custom type. For e.g. when authoring a library or a web framework. In that case, maybe we want consumers of the library to embed the base controller into the custom controller to get functionality out of it.
package main
import "fmt"
type Animal struct {
name string
origin string
}
type Bird struct {
Animal
wingspan float32
canFly bool
}
/** Declaring and manipulating from the outside */
func main() {
bird := Bird{
wingspan: 20.25,
canFly: true,
}
bird.name = "Bald Eagle"
bird.origin = "North America"
/** {{Bald Eagle North America} 20.25 true} */
fmt.Println(bird)
fmt.Println(bird.name) // "Bald Eagle"
/** Declaring using the literal syntax */
bird2 := Bird{
Animal: Animal{
name: "Bald Eagle",
origin: "North America",
},
wingspan: 20.25,
canFly: true,
}
/** {{Bald Eagle North America} 20.25 true} */
fmt.Println(bird2)
fmt.Println(bird.name) // Bald Eagle
}
Structs: Tags
- Tags describe specific metadata about a field of a struct.
- All tags do is provide a string of text. It's upto the validation framework to do something with tags. By themselves tag bring in no functionality.
package main
import (
"fmt"
"reflect"
)
type Person struct {
id int `required:"true"`
name string `required:"true"`
}
func main() {
person := Person{
id: 1,
name: "Jayanta",
}
t := reflect.TypeOf(person)
field, _ := t.FieldByName("name")
fmt.Println(person) // {1 Jayanta}
fmt.Println(person.name) // Jayanta
fmt.Println(field.Tag) // required:"true"
fmt.Println(field.Tag.Get("required")) // true
}
Conditionals
If Statements
- The curly braces are a necessary Go syntax for if statements
- Variables declared within an if block are block-scoped to that particular block.
- First brackets are permissible to be used for the logic part of the if statement.
if a > b
andif (a > b)
are both valid ways of declaring an if statement.
If, If-else, else Statements
package main
import (
"fmt"
)
func main() {
a := 12
b := 20
if a > b {
fmt.Println("'a' is greater than 'b'")
} else if a == b {
fmt.Println("'a' is equal to 'b'")
} else {
fmt.Println("'b' is greater than 'a'")
}
}
Using the Initializer Syntax
- Syntax:
if initializer; boolean {}
package main
import (
"fmt"
)
func main() {
timeZone := map[string]int{
"UTC": 0*60*60,
"IST": 5*60*60 + 0.5*60*60,
"EST": -5*60*60,
"CST": -6*60*60,
"MST": -7*60*60,
"PST": -8*60*60,
}
if time, ok := timeZone["IST"]; ok {
fmt.Println(time) // executes this line
} else {
fmt.Println("IST not present")
}
}
Equality with Floating Point Numbers
When working with floating point numbers, you may run into an issue with equality because the return value of floating point operations are approximations of decimal values and not exact.
Example:
package main
import (
"fmt"
"math"
)
func main() {
num = 0.123
if num == math.Pow(math.Sqrt(num), 2) {
fmt.Println("These are the same!")
} else {
fmt.Println("These are different!") // executes this line
}
}
Hence when we are doing comparison operations with decimal values, a better approach is to have a small error margin when doing the comparison.
package main
import (
"fmt"
"math"
)
func main() {
num := 0.1234141794174194
if math.Abs(math.Pow(math.Sqrt(num), 2)) - 1 < 0.0001 {
fmt.Println("These are the same!") // executes this line
} else {
fmt.Println("These are different!")
}
}
This isn't a perfect solution but the decimal error margin can be further tweaked to fine-tune the error margin.
Switch Statements
Go's switch is more general than C's. The expressions need not be constants or even integers, the cases are evaluated top to bottom until a match is found, and if the switch has no expression it switches on true. It's therefore possible—and idiomatic—to write an if-else-if-else chain as a switch.
Some characteristics to keep in mind with Switch statements
- There can be multiple tests in a single case
- Instead of a simple tag, can use an initializer like the if statement.
- Can use a tagless syntax in which there is no value evaluated at the switch statement and the cases behave like if-else-if-else blocks.
- There cannot be overlapping cases EXCEPT when using a tagless syntax. Will throw a syntax error.
- The
break
after each case statement is implicit in Go, as a design decision, instead of the default fallthrough. However if we want to exit a single case statement early for some reason we can use thebreak
keyword explicitly. - If we do intend a fallthrough behaviour for a particular case, we can specify the keyword
fallthrough
. The fallthrough will override any case logic in the next case and execute the code directly.
Simple Cases
package main
import "fmt"
func main() {
num := 2
switch num {
case 1:
fmt.Println("one")
case 2:
fmt.Println("two") // executes this line
default:
fmt.Println("other number")
}
}
Cases with Multiple Tests
package main
import "fmt"
package main
import "fmt"
func main() {
num := 6
switch num {
case 0:
fmt.Println("Number must be greater than 0")
case 1, 3, 5, 7, 9:
fmt.Println("odd")
case 2, 4, 6, 8:
fmt.Println("even") // executes this line
default:
fmt.Println("Number must be between 1 and 10")
}
}
Using an Initializer
package main
import (
"fmt"
"math"
)
func main() {
const num = -1
switch i := math.Max(num, 0); i {
case 0:
fmt.Println("Number must be greater than 0") // executes this line
case 1, 3, 5, 7, 9:
fmt.Println("odd")
case 2, 4, 6, 8:
fmt.Println("even")
default:
fmt.Println("Number must be between 1 and 10")
}
}
Tagless Switch Statement
package main
import "fmt"
func main() {
num := 10
switch {
case num <= 0:
fmt.Println("Number must be greater than 0")
case num > 10:
fmt.Println("Number must be between 1 and 10")
case num % 2 == 1:
fmt.Println("odd")
default:
fmt.Println("even") // executes this line
}
}
Falling Through
We can use the keyword fallthrough
to make a case statement not break when it executes but fall through to the next case statement. fallthrough
does not evaluate any logic, and executes the next case regardless of the case logic in that particular case. It is upto the developer to implement the control flow.
package main
import "fmt"
func main() {
num := 10
switch {
case num <= 10:
fmt.Println("Number is less than or equal to 10") // executes this line
fallthrough
case num <= 20:
fmt.Println("Number is less than or equal to 20") // executes this line
default:
fmt.Println("Number is greater than 20")
}
}
Type Switch
A switch can also be used to discover the dynamic type of an interface variable. Such a type switch uses the syntax of a type assertion with the keyword type inside the parentheses. If the switch declares a variable in the expression, the variable will have the corresponding type in each clause. It's also idiomatic to reuse the name in such cases, in effect declaring a new variable with the same name but a different type in each case.
package main
import "fmt"
func main() {
var i interface{} = 10
switch i.(type) {
case int:
fmt.Printf("i is int %v\n", i) // i is int 10
if (i == 10) {
fmt.Println("i is 10") // executes this line
break // breaks here
}
fmt.Println("Will not execute this line") // Doesn't execute
case bool:
fmt.Printf("i is boolean %v\n", i)
case string:
fmt.Printf("i is int %v\n", i)
default:
fmt.Printf("i is another type: %T %v\n", i, i)
}
}
Loops
Things to remember while working with Loops in Go:
- The increment/decrement operation in Go is not an expression, it is a statement on its own.
- The traditional
do-while
loop can be implemented by running a for-loop with only the conditional and the counter initialized outside the for-loop and controlled from within. - You can manually short-circuit out of a for-loop by using
break
. - To end the particular iteration of the loop and go to the next without running the code below, we can use
continue
.
package main
import "fmt"
func main() {
/** Basic for-loop */
for i := 0; i < 5; i++ {
fmt.Println(i)
}
for i := 0; i < 10; i+=2 {
fmt.Println(i)
}
/** Looping using multiple variables */
for i, j := 0, 0; i < 5; i, j = i+1, j+2 {
fmt.Println(i, j)
}
/** Leaving the first statement out. Note: `i` is scoped outside of the for-loop */
i := 0
for ; i < 5; i++ {
fmt.Println(i)
}
/** This is how Go implements do-while loops */
i := 0
for i < 5 {
fmt.Println(i)
i++
}
/** Running loops that require complex logic and manual short-circuit */
i := 0
for {
if (i == 10) {
break
}
i++
if(i % 2 == 1) {
continue
}
fmt.Println(i) // Prints the even numbers from 0 - 10, i.e. 2 4 6 8 10
}
/** Nested for-loops with labels */
Loop:
for i := 1; i <= 10; i++ {
for j := 1; j <= 3; j++ {
fmt.Println(i * j)
if (i * j >= 3) {
break Loop // breaks the outer-loop defined as the label `Loop`
}
}
}
/** Working with collection types */
s := []int{1, 2, 3, 4, 5}
var timeZone = map[string]int{
"UTC": 0*60*60,
"EST": -5*60*60,
"CST": -6*60*60,
"MST": -7*60*60,
"PST": -8*60*60,
}
/** Method 1: Works for strings, arrays and slices */
for i := 0; i < len(s); i++ {
fmt.Println(s[i]) // Prints each element out
}
/** Method 2: Works with strings, arrays, slices, maps and channels */
// On slices, arrays
for index, val := range s {
fmt.Println(index, val)
}
// On maps
for key, val := range timeZone {
fmt.Println(key, val) // Prints the key and value out
}
/** A situation where we need only the value */
for _, val := range timeZone {
fmt.Println(val) // Prints the key and value out
}
}
We can also range over another data type called Channels. Channels are used for concurrent programming in Go and a topic for a future section.
Defer, Panic and Recover
Defer
In a normal Go application, control flows from the top to the bottom of any function that we call.
With defer
we can invoke a function but delay its execution time to a future point in time.
Here are some important points to remember about deferred functions:
- Takes a function call (not a function itself)
- The way
defer
works is that it executes any functions that are passed into it, after the function finishes its final statement but before it returns. - Deferred functions are executed in LIFO (Last-in-first-out) order.
- Deferred functions take arguments at the time of the deferred function is called, not at the time the function is executed.
- Care should be exercised when it comes to loops.
package main
import "fmt"
func main() {
fmt.Println("start")
defer fmt.Println("middle") // second last defer function executes second
defer fmt.Println("middle2") // last defer function executes first
fmt.Println("end")
}
/** Prints
start
end
middle2
middle
*/
Deferring functions whose arguments maybe modified later
package main
import "fmt"
func main() {
a := "start"
a = "middle"
defer fmt.Println(a) // defer prints "middle" as it takes the argument at the time of defer and not during execution
a = "end"
}
Use Case: Associate the opening and the closing of a resource right next to each other.
package main
import (
"fmt"
"io"
"log"
"net/http"
)
func main() {
res, err := http.Get("https://www.google.com/robots.txt")
if (err != nil) {
log.Fatal(err)
}
defer res.Body.Close()
robots, err := io.ReadAll(res.Body)
if(err != nil) {
log.Fatal(err)
}
fmt.Printf("%s", robots)
}
Panic
In Go, we don't have exceptions, like how many programming languages have, because a lot of what is considered exceptions in other languages are considered normal in Go. For example: If you try to open a file that doesn't exist, Go returns an error value but doesn't throw an exception.
However, sometimes, a Go application can enter a state where it can no longer continue to run. This can be triggered by the Go runtime but we can also trigger this on our own.
Here are some characteristics of panic
statements:
- Can be manually triggered or triggered by the Go runtime.
- Panics happen after deferred statements are executed.
package main
import "fmt"
func main() {
fmt.Println("start") // This is executed first
defer fmt.Println("This is deferred!") // This is executed third
fmt.Println("middle") // This is executed second
panic("something bad happened")
fmt.Println("end") // doesn't show as application panics before.
}
Using panic manually when running a http server
package main
import "net/http"
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello Go!"))
})
err := http.ListenAndServe(":8080", nil)
if(err != nil) {
panic(err.Error())
}
}
Recover
When the application starts to panic, ideally there should be some way to recover. When panic is called, including implicitly for run-time errors such as indexing a slice out of bounds or failing a type assertion, it immediately stops execution of the current function and begins unwinding the stack of the goroutine, running any deferred functions along the way. If that unwinding reaches the top of the goroutine's stack, the program dies. However, it is possible to use the built-in function recover to regain control of the goroutine and resume normal execution.
A call to recover stops the unwinding and returns the argument passed to panic because the only code that runs while unwinding is inside deferred functions, recover is only useful inside deferred functions.
One application of recover is to shut down a failing goroutine inside a server without killing the other executing goroutines.
- The proper place to use a recover function is inside a deferred function as while panic stops executing any other statements, it will execute deferred functions.
package main
import (
"fmt"
"log"
)
func main() {
fmt.Println("start") // This is executed first
panicker()
fmt.Println("end") // This is executed last
}
func panicker() {
fmt.Println("About to panic") // This is executed second
defer func() {
if err := recover(); err != nil {
log.Println("Error:", err) // This is executed third
// can rethrow the panic
}
}()
panic("Something bad happened")
fmt.Println("Done panicking") // This does not execute
}
Pointers
Pointers hold a reference in memory about another variable. By default all primitive types are value types.
The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers.
Pointers: Creation
Pointers can be created by adding a *
to the type during declaration (adds a pointer to the data of the type) and assigning another variable with the address-of (&
) operator.
The *
operator can also be used to dereference a numerical representation of a memory location to get it's actual value by prefixing a pointer with it.
package main
import "fmt"
func main() {
var a int = 42
var b *int = &a // numeric representation for the memory address that is holding the location of a
fmt.Println(a, b) // 42 0xc0000a2000
fmt.Println(&a, b) // 0xc0000a2000 0xc0000a2000
fmt.Println(a, *b) // 42 42 (dereferenced)
/** Modify all Pointers by changing the original value directly */
a = 30
fmt.Println(a, *b) // 30 30
/** Changing the deferenced b changes the original value */
*b = 50
fmt.Println(a, *b) // 50 50
}
Now, if you come from a background in languages that allow you to work with pointers as variables, then you might be able to do something called Pointer Arithmetic. Go does not allow Math to be done in the same way.
If you've come from C or C++, you are probably aware of the tremendous performance benefits and advantages if you're allowed to do Pointer Arithmetic because you can jump around memory pretty quickly and get performance benefits in certain applications. However, when you're getting into Pointer Arithmetic, you're getting into some fairly complex code and since one of Go's core design concerns is Simplicity, the decision was made to leave Pointer Arithmetic out of the Go language.
package main
import "fmt"
func main() {
a := [3]int{1, 2, 3}
b := &a[0]
c := &a[1]
d := &a[1]
// e := &a[1] - 8 // will throw an error
fmt.Printf("%v %p %p %p\n", a, b, c, d) // [1 2 3] 0xc00001a1e0 0xc00001a1e8 0xc00001a1f0
}
We can notice that there's difference of 8 between the memory locations.
Since a
is an array of integers and integers in this version of the runtime are 8 bytes long, each element in the array are 8 bytes apart.
Note: If at all you need to do something like Pointer Arithmetic, we can use the [
unsafe
] (https://pkg.golang.ir/unsafe) package for those advanced scenarios. This package contains operations that step around the type safety of Go programs and is not checked by the Go runtime.
So far, we've always declared the underlying type first. But that's often not necessary in Go, because often only want to work with the Pointers and don't really care where the underlying data is stored, you just need the ability to point to it wherever that is.
package main
import "fmt"
type myStruct struct {
foo int
}
func main() {
var ms *myStruct
ms = &myStruct{
foo: 40,
}
fmt.Println(ms) // &{40}
/** Alternative declaration */
ms2 := &myStruct{foo: 40}
fmt.Println(ms2) // &{40}
fmt.Println(ms2.foo) // 40
}
Pointers: Working with nil
A Pointer that you didn't initialize will be initialized for you and is going to hold the value nil
. When accepting Pointers as arguments, it is best practice to check if the Pointer is a nil
pointer or not, because if it is, we have to handle it in a different way. For e.g. we have a Pointer to a struct and we want to drill through and get to a field, but if the Pointer is nil
, we are going to get a runtime exception.
Example of a nil
pointer:
package main
import "fmt"
type myStruct struct {
foo int
}
func main() {
var ms *myStruct
fmt.Println(ms) // <nil>
ms = new(myStruct) // Initialize a Pointer but without any fields at the same time
fmt.Println(ms) // &{0}
}
Working with a pointer that is nil
:
package main
import "fmt"
type myStruct struct {
foo int
}
func main() {
var ms *myStruct = new(myStruct)
fmt.Println(ms) // &{0}
(*ms).foo = 40 // Deferencing operator has a lower precedence than the dot operator, hence we need to ensure we dereference the struct and not the struct.field
fmt.Println((*ms).foo) // 40
}
Go however has a syntactic sugar that makes it easy to work with pointers. Instead of constantly dereferencing we can do the same operation as above by getting rid of the dereferencing operator completely:
package main
import "fmt"
type myStruct struct {
foo int
}
func main() {
var ms *myStruct = new(myStruct)
fmt.Println(ms) // &{0}
ms.foo = 40
fmt.Println(ms.foo) // 40
}
This might be freaking out those that are coming from a C/C++ background because the Pointer ms
doesn't actually have a field foo
on it. The pointer ms
is pointing to a struct
that has a field foo
. So how does this work? In Go, this is syntactic sugar where the Go compiler understands that we are not actually trying to access the foo
field on the pointer but implying that we want to access the underlying struct
.
In the next sections, we will get into the benefits of using Pointers.
Functions
Functions: Overview
package main
import "fmt"
func main() {
sayGreeting("Hello", "Jayanta")
}
/** Function with arguments of the same type */
func sayGreeting(greeting, name string) {
fmt.Println(greeting, name)
}
Functions: Accepting Pointers as Parameters
There are some reasons why you'd want to accept pointers as parameters:
- A lot of times, functions do need to act on the parameters that are passed into them. Passing in Pointers is the only way to do that.
- Passing in a Pointer is often much more efficient than passing in a whole value, especially when it comes to large data structures, as passing in a value would mean copying in that data structure every single time the function is called.
package main
import "fmt"
func main() {
greeting := "Hello"
name := "Bhargav"
sayGreeting(&greeting, &name)
fmt.Println(name) // Jayanta | Since passed as reference, `name` is mutated by the function
}
func sayGreeting(greeting, name *string) {
fmt.Println(*greeting, *name) // Hello Bhargav
*name = "Jayanta"
fmt.Println(*name) // Jayanta
}
Functions: Variadic Parameters
Variadic Parameters are the equivalent of Rest Parameters in JavaScript/TypeScript and Packing in Python. It allows a function to take in an unknown number of arguments. In Go, it takes those unknown number of arguments and wraps them into a slice that has the name of the variable. Just like JavaScript or Python, where the Rest Parameters/Packing have to be the last parameter in a function, Variadic Parameters have to be the last parameter of a function declaration.
package main
import "fmt"
func main() {
sum("Sum:", 1, 2, 3, 4, 5)
}
func sum(msg string, values ...int) {
fmt.Println(values) // [1 2 3 4 5]
result := 0;
for _, v := range values {
result += v
}
fmt.Println(msg, result) // Sum: 15
}
Functions: Returning from a Function
In Go, we can use a return
statement to return a value or a pointer from a function. Like all programming languages, the return statement returns the value or pointer and then exits the function.
Here are some points to remember when working with functions with a return value:
-
The type of the return value must be at the function declaration, right after the arguments are declared and before the opening of the curly braces.
-
One of Go's unusual features is that functions and methods can return multiple values. For functions that return multiple return values, enclose the return value types inside brackets and separate them with commas.
-
Another feature of Go, that is rare in programming languages is the ability to return a local variable as a Pointer.
Important: If you're coming from another language that uses Pointers a lot and doesn't abstract away the differences between working on the Stack and working on the Heap, this might freak you out a bit. But what's happening is, when we declare the local variable inside the function, it is actual declared on the Execution Stack of the function, which is just a special section of memory set aside for all of the operations that the function is going to be working with. So when we return a local variable as a Pointer, when the function exits, the Execution Stack is destroyed and the memory is freed up.
In many languages, this is not a safe operation, as now you're returning a Pointer to a location in memory that just got freed and you've no idea what value is going to be there.
But in the Go language, when the Go compiler recognizes that you're returning a value that is generated on the local stack, it's automatically going to promote this variable for you, to be on the shared memory of the computer, i.e. the Heap memory. Thus, the Go runtime is going to recognize that you are returning a Pointer from the local stack and everything works as usual.
-
Can return a named return value. In that case the variable is initialized with the default initialization value and just the
return
keyword is enough to return the value.
Function that returns a single return value:
package main
import "fmt"
func main() {
s := sum(1, 2, 3, 4, 5)
fmt.Printf("The sum is: %v\n", s) // The sum is: 15
}
func sum(values ...int) int {
fmt.Println(values) // [1 2 3 4 5]
result := 0;
for _, v := range values {
result += v
}
return result // 15
}
Function that returns multiple return values:
Note: Below is a common pattern to return a result and an error from a function
package main
import "fmt"
func main() {
d, err := divide(5.0, 0.0)
if err != nil {
fmt.Println(err) // Cannot divide by zero
return
}
fmt.Printf("Result: %v\n", d)
}
func divide(a, b float64) (float64, error) {
if b == 0.0 {
return 0.0, fmt.Errorf("Cannot divide by zero")
}
return a / b, nil
}
Returning a Pointer instead of a Value:
Same rules of working with Pointers apply.
package main
import "fmt"
func main() {
msg, s := sum("The sum is:", 1, 2, 3, 4, 5)
fmt.Println(*msg, *s) // The sum is: 15
}
func sum(msg string, values ...int) (*string, *int) {
fmt.Println(values) // [1 2 3 4 5]
result := 0;
for _, v := range values {
result += v
}
return &msg, &result // "The sum is:", 15
}
Using named return values:
package main
import "fmt"
func main() {
s := sum(1, 2, 3, 4, 5)
fmt.Printf("The sum is: %v\n", s) // The sum is: 15
}
func sum(values ...int) (result int) {
fmt.Println(values) // [1 2 3 4 5]
for _, v := range values {
result += v
}
return // 15
}
Functions: Anonymous Functions
An unnamed function declared on the fly that is immediately executed is called an anonymous function.
Note: For those coming from JavaScript / TypeScript, this is the equivalent of IIFE (Immediately Invoked Function Expressions).
package main
import "fmt"
func main() {
func() {
fmt.Println("Hello World!")
}()
}
Functions: Functions as Types
In Go, functions themselves can be treated as types that can be passed around as variables, passed around as arguments in a function, as return values, pretty much anything you can do with any other type.
Note: When assigning functions to variables, ensure the function is called after the function is declared and assigned to the variable.
package main
import "fmt"
func main() {
f := func (args ...int)(result int) {
for _, val := range args {
result += val
}
return result
}
fmt.Println("The sum is:", f(1, 2, 3, 4, 5))
}
Functions: Methods
A method is just a function that is executing in a known context. A known context in Go, is ANY type.
Below is an example with the struct
type, which is a common use case:
package main
import "fmt"
type greeter struct{
greeting string
name string
}
func main() {
g := greeter{
greeting: "Hello",
name: "Jayanta",
}
g.greet() // Hello, Jayanta Samaddar
fmt.Println(g.name) // Jayanta (unchanged as passed as Value)
g.welcome("Samaddar") // Jayanta Samaddar, you are welcome!
fmt.Println(g.name) // Jayanta Samaddar (changed as passed as Pointer)
}
/********************************************************************************************************************/
// Working with Receivers
// ----------------------
// Note: Can only use one receiver for a method.
// To get around duplication of methods for different receivers, declare the method types as an Interface and reuse
// (Check the next section for Interfaces)
/********************************************************************************************************************/
// Receiving as a Value Receiver: Cannot mutate the original receiver object as a copy is used
func (g greeter) greet() {
g.name = "Jayanta Samaddar" // Re-assigning doesn't work outside the local scope
fmt.Printf("%v, %v\n", g.greeting, g.name)
}
// Receiving as a Pointer Receiver: Can mutate the original receiver object as a pointer to the original is used
func (g *greeter) welcome(surname string) {
g.name = "Jayanta Samaddar" // Re-assigning modifies the outside scope
fmt.Printf("%v, %v\n", g.name, "you are welcome!")
}
Functions: String()
method for a Custom Type
If you want to control the default format for a custom type, all that's required is to define a method with the signature String()
string on the type. For our simple type T
, that might look like this.
func (t *T) String() string {
return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
fmt.Printf("%v\n", t)
For a detailed example, check the LinkedList implementation
Interfaces
Interfaces: Basics
Interfaces in Go provide a way to specify the behavior of an object: if something can do this, then it can be used here. Unlike structs, which are data containers, interfaces do not describe data but behaviour.
If you're coming from another language, you maybe looking for an implements
keyword. In Go, we do not explicitly implement interfaces. We implicitly implement the interface, by creating a method on the type that has the signature of the interface.
We've seen a couple of simple examples already; custom printers can be implemented by a String method while Fprintf can generate output to anything with a Write method. Interfaces with only one or two methods are common in Go code, and are usually given a name derived from the method, such as io.Writer for something that implements Write.
Implementation of an Interface using a struct
:
package main
import "fmt"
type Writer interface {
Write([]byte) (int, error)
}
type ConsoleWriter struct {}
func (cw ConsoleWriter) Write(data []byte) (int, error) {
n, err := fmt.Println(string(data))
return n, err
}
func main() {
var w Writer = ConsoleWriter{}
w.Write([]byte("Hello, Jayanta!")) // Hello, Jayanta
}
Implementation of an Interface using a int
:
package main
import "fmt"
type Incrementer interface {
Increment() int
}
type IntCounter int
func (ic *IntCounter) Increment() int {
*ic++
return int(*ic)
}
func main() {
myInt := IntCounter(0)
var inc Incrementer = &myInt
for i := 0; i < 5; i++ {
fmt.Println(inc.Increment()) // Prints 1 to 5
}
}
Interfaces: Composing Interfaces
In Go, we can compose interfaces together and is a common pattern that is the key to scalability. We can compose interface by using the same concept of embedding as we used in structs.
package main
import (
"fmt"
)
type Writer interface {
Write([] byte) (int, error)
}
type Closer interface {
Close() error
}
type WriterCloser interface {
Writer
Closer
}
type myWriterCloser struct {}
func (mwc myWriterCloser) Write([]byte) (int, error) {
return 0, nil
}
func (mwc myWriterCloser) Close() error {
return nil
}
func main() {
var wc WriterCloser = myWriterCloser{}
fmt.Println(wc)
}
Interfaces: Type Conversion
The Empty Interface
An empty interface is an interface defined on the fly that has no methods on it. The benefit of the empty interface is that everything can be cast into an object that has no methods on it, even primitives. The empty interface can be very useful in situations where you have multiple things that you need to be working with, but they aren't type compatible with one another and you need to apply some logic later to figure out how exactly what you receive.
The catch is that you cannot do anything with an empty interface unless we do either type conversion or use the Reflect package, in order to figure out what to do with it.
Type Switches
package main
import "fmt"
func main() {
var i interface{} = 10
switch i.(type) {
case int:
fmt.Printf("i is int %v\n", i) // i is int 10
if (i == 10) {
fmt.Println("i is 10") // executes this line
break // breaks here
}
fmt.Println("Will not execute this line") // Doesn't execute
case bool:
fmt.Printf("i is boolean %v\n", i)
case string:
fmt.Printf("i is int %v\n", i)
default:
fmt.Printf("i is another type: %T %v\n", i, i)
}
}
Interfaces: Implementation
- For an interface implemented with a value type, all methods of that interface, have to have Value Receivers.
- When implementing the interface with a pointer type, the receivers can have either values or pointers.
Implementation with Pointers:
package main
import (
"fmt"
)
type Writer interface {
Write([] byte) (int, error)
}
type Closer interface {
Close() error
}
type WriterCloser interface {
Writer
Closer
}
type myWriterCloser struct {}
func (mwc *myWriterCloser) Write([]byte) (int, error) {
return 0, nil
}
func (mwc myWriterCloser) Close() error {
return nil
}
func main() {
var wc WriterCloser = &myWriterCloser{}
fmt.Println(wc) // &{}
}
Interfaces: Best Practices
When working with Interfaces there are some rules and guidelines to keep in mind:
-
Prefer many, small interfaces v/s large, monolithic interfaces: Some of the powerful interfaces in the Go language, e.g.
io.Reader
,io.Writer
,interface{}
, have one or zero methods on them. -
Don't export interfaces for types that will be consumed: If you're coming from a language that explicitly implements interfaces, you'll be very tempted to create interfaces and export them. In Go, since there is implicit implementation of interfaces, if you don't need to export the interface yourself, if there's no particular reason to do it, one shouldn't export interface and simply export the concrete type. E.g. In the
database/sql
package, thedb
object is exported as a concrete struct that had other methods that pointed to other concrete structs. So, on one hand, you cannot directly mock that for testing right out of the box, however, by not exporting an interface, it allows you, as the consumer of the struct to define your own interface that you can use for testing. The beauty of that is that is that if you don't use some of the methods of thedb
object, your interface does not have to have those methods on it - you can just expose the methods that you need. -
Do export interfaces for types that will be used by the package: This is almost backwards to how other languages implement interfaces because of implicit implementation of interfaces in Go, you can defer the creation of the interfaces until exactly when you need them. So, if you are creating a library other people are going to consume, you can define the interfaces that you accept and make the users provide whatever implementations that they want. If your library has reasonable defaults, you can export those concrete types, but make sure you are accepting interfaces whenever possible. That leads us to the next point.
-
Designs functions and methods to receive interfaces whenever possible: While this is not always possible - if you need access to the underlying data field, take in the concrete type, but if you are accepting behaviour providers, then accept those as interface types instead of concrete types.
Goroutines
Concurrency is the ability of an application to handle multiple tasks which start, run, and complete in overlapping time periods, in no specific order.
Goroutines: Fundamentals - Processes and Threads
-
Processes: In the context of operating systems, a process is an independent program with its own memory space, resources, and execution environment. Each Go program runs as a separate process. Multiple instances of a Go program can run concurrently as separate processes on a machine. Processes are isolated from each other, and communication between them often involves inter-process communication (IPC) mechanisms. E.g. Notepad can be run as two difference instances of the same program, i.e. two processes - They do not share memory space, resources and execution environment, i.e. have isolation from each other. Thus, if one process by any chance crashes, the other will be unaffected. The disadvantage of processes is that, they can be heavyweight as they need their own memory space and other resources and takes a longer time to start.
An application can also contain multiple processes within itself. We might know of a function called
fork
in C or C++ which spawns a new process programatically. As soon asfork
is called, the entire memory space and program counter is copied into a separate process called a child process. These two processes (parent and child process) now continue with their own individual lifecycles.When using processes to solve a problem, we would break a problem into multiple parts and have each process solve a particular segment of the problem and then stitch it all up in the end. There is not a lot of communication between the processes. In Go, the support for creating and using processes isn't that great and is provided by the
os/exec
package, and it is relatively high-level and convenient compared to C or C++. In fact, Go does not have direct equivalents to thefork
andspawn
functions found in languages like C or even Node.js. -
Threads: Threads are smaller units of execution within a process. A process can have multiple threads, and these threads share the same memory space and resources. Traditionally, operating systems manage threads, and threads within the same process can communicate with each other using shared memory.
When using threads to solve a problem, we would solve the problem directly with all the threads involved in solving it. Thus, this requires communication between threads for efficiency.
In Go, the language runtime manages its own internal threads or green threads (called goroutines). Developers typically work with goroutines rather than directly managing threads.
Goroutines: Green Threads
A green thread is a more efficient version of a thread. To understand why it is more efficient, we need to understand traditional Operating system threads and to understand why we need to make traditional threads faster, we need to understand an Operating System concept called Context Switching.
Context Switching
Context switching is a process performed by the operating system's scheduler to switch the execution context from one thread or process to another. It involves saving the current state of a thread or process (its context) and restoring the saved state of another thread or process so that it can resume execution.
The context of a thread or process includes information such as the contents of the CPU registers, the program counter (the address of the next instruction to be executed), the stack pointer (indicating the current position in the program's call stack), and other relevant information needed for the thread or process to continue its execution.
Here are the key steps involved in a context switch:
-
Save Context: The operating system saves the context of the currently running thread or process. This involves storing the values of CPU registers, the program counter, and other necessary information into a data structure known as the process control block (PCB).
-
Load Context: The operating system selects the next thread or process to run and loads its context from its PCB. This involves restoring the values of CPU registers, the program counter, and other relevant information.
-
Switch to User Mode: If the context switch involves switching between user mode and kernel mode, the operating system switches to the user mode to allow the selected thread or process to execute.
-
Resume Execution: The selected thread or process now resumes execution from the point where it was previously interrupted. The program counter is set to the next instruction to be executed, and the CPU begins executing instructions for the newly scheduled thread or process.
Context switching is a fundamental operation in multitasking and multiprocessing environments, where multiple threads or processes share a single CPU. It allows the operating system to provide the illusion of concurrent execution by rapidly switching between different threads or processes, giving the appearance that they are running simultaneously. Efficient context switching is crucial for maintaining system responsiveness and supporting concurrent execution of multiple tasks.
Context switch overhead refers to the cost associated with performing a context switch between two threads or processes in a multitasking or multiprocessing system. The overhead arises from the time and resources required to save the state of the currently running thread or process, load the state of another thread or process, and perform any necessary operations associated with the context switch.
The context switch overhead is generally considered undesirable, as it can impact system performance and responsiveness. Reducing context switch overhead is a goal in the design and optimization of operating systems and scheduling algorithms. Techniques such as prioritized scheduling, preemption control, and minimizing memory operations help mitigate context switch overhead and improve overall system efficiency.
A green thread aims to improve the efficiency of traditional threads by reducing this context switch overhead. When there are thousands of threads running, the operating system spends a lot of time doing context switching and lot less time doing useful execution. Green threads aren't something new that was invented recently. The correct technical term for green threads are "user level threads".
Traditional Threads vs Green Threads
There are two types of threads:
-
Operating System Threads: Operating system threads, also known as kernel-level threads, are managed by the operating system's kernel. Each thread is represented and scheduled by the operating system, and the kernel is responsible for saving and restoring the thread's context during context switches.
- Isolation and Resources: OS threads have their own stack and register set, providing strong isolation between threads. They can run concurrently and independently of each other. Resources such as file descriptors, open files, and memory space are typically isolated between threads.
- System Calls: Blocking operations, such as I/O operations, often result in the entire thread being blocked. The operating system scheduler can switch to another thread during such blocking calls.
- Kernel Synchronization: The kernel is responsible for thread synchronization, and synchronization mechanisms like mutexes and semaphores are commonly used to coordinate access to shared resources.
-
Green Threads: Green threads, also known as user-level threads or fibers, are managed entirely by a runtime or a user-level library. They are not directly associated with the operating system kernel. Green threads operate in user space, and the runtime or library (program) is responsible for scheduling and context switching between threads. A green thread typically runs inside a kernel-level thread and typically many green threads are running inside one kernel-level thread. Using this strategy the kernel doesn't need to get involved to do the context switching, instead it is the program that can decide what to execute. Thus green threads are faster when handling context-switching, especially in scenarios with a large number of concurrent tasks (thousands of green threads).
- Lightweight: Green threads are typically lightweight compared to OS threads. They share the same stack and heap, and context switches between green threads involve saving and restoring a smaller set of user-level state.
- Cooperative Scheduling: Green threads often operate under cooperative scheduling. Each thread voluntarily yields control to the scheduler at certain points, allowing other threads to run. Cooperative scheduling is often managed by language runtimes (e.g., Go, Python, Java) or user-level libraries.
- Reduced Kernel Involvement: Since green threads are managed entirely in user space, there is no need for the operating system to be aware of them. Context switches between green threads do not involve the kernel. This can be beneficial in scenarios where fine-grained control over concurrency is required.
- Avoid Kernel Locks: Green threads may avoid contention for kernel locks associated with traditional context switches. This can be advantageous in scenarios with high concurrency, where contention for kernel resources can be a limiting factor.
Differences between Operating System Threads and Green Threads:
Comparison | Operating System Threads | Green Threads |
---|---|---|
Concurrency Model | OS threads provide true parallelism, as they can run concurrently on multiple processors or cores. | Green threads are often used to achieve concurrency and parallelism in a user-level fashion, but they may not take full advantage of multiple cores unless the runtime or library provides some form of parallelism (e.g., goroutines in Go). |
Resource Usage | OS threads tend to have higher resource overhead due to separate stacks and registers for each thread. | Green threads have lower resource overhead as they share the same stack and heap, but they may be limited by the available resources of a single OS thread. |
Synchronization | OS threads use kernel-level synchronization mechanisms. | Green threads often rely on user-level synchronization mechanisms, which can be more lightweight but may lack some of the guarantees provided by kernel-level synchronization. |
Blocking Operations | OS threads can block independently, potentially allowing other threads to continue execution. | Green threads may need to be explicitly designed for cooperative scheduling to avoid blocking the entire runtime. |
Goroutines: Threads in Go and Goroutines
Go uses a hybrid system of kernel-level threads and green threads. It creates a kernel-level thread for each CPU available and each one of these threads contains a number of the green threads. Whenever a green thread tries in Go tries to do a synchronous operation where we have to wait for I/O and the whole thread is removed from execution, Go reshuffles those green threads that are not doing any I/O work or waiting on any operation to other available kernel-level threads that are still running on the CPU. This way, it tries to avoid the slow kernel-level context switching and improve the problem of waiting on I/O from a single green thread.
From a Go developer's perspective, the key takeaway is that Go abstracts away many of the complexities associated with traditional threading. Developers can focus on writing concurrent code using goroutines without worrying about low-level thread management. This makes it easier to write scalable and concurrent programs in Go. The Go runtime scheduler efficiently handles the execution and coordination of goroutines, making concurrent programming in Go more accessible and less error-prone compared to traditional thread-based approaches.
Goroutines are a concurrency primitive in Go, and they are sometimes referred to as "green threads" or "lightweight threads." Goroutines are managed by the Go runtime, and they are much lighter than traditional threads. Many goroutines can run concurrently within a single operating system thread. Goroutines are created using the go
keyword, and they are functions that can be executed concurrently with other goroutines. The Go runtime scheduler multiplexes goroutines onto operating system threads, providing efficient concurrency without the need for manual thread management by developers.
Goroutines: Deep Dive - How Go Multiplexes Goroutines (Green Threads) onto Operating System Threads
In Go, the multiplexing of green threads (goroutines) onto OS threads is handled by the Go runtime scheduler. The scheduler is an integral part of the Go runtime, and its primary responsibility is to manage the execution of goroutines on the available OS threads.
Here are the key aspects of how Go accomplishes the multiplexing:
-
Goroutine Scheduling:
- When a new goroutine is created using the
go
keyword, it is scheduled to run by the Go runtime scheduler. - The scheduler decides when to start, pause, or resume the execution of goroutines. This scheduling is done cooperatively, meaning that goroutines voluntarily yield control to the scheduler during certain points in their execution.
- When a new goroutine is created using the
-
OS Thread Pool:
- Go runtime maintains a pool of OS threads, also known as the "M" (machine) pool. These OS threads are managed by the runtime and are used to execute goroutines.
- The number of OS threads in the pool is dynamic and can be adjusted based on the workload and system characteristics.
-
G-M-P Model:
- The scheduling model in Go is often referred to as the G-M-P model, where:
- G (Goroutine): Represents a goroutine.
- M (Machine): Represents an OS thread (machine).
- P (Processor): Represents a context that holds resources needed for scheduling (e.g., a local queue of goroutines).
- The scheduler maps goroutines (G) onto OS threads (M) and manages the distribution of goroutines among the available OS threads.
- The scheduling model in Go is often referred to as the G-M-P model, where:
-
Global Run Queue and Local Queues:
- There is a global run queue that contains all runnable goroutines. The scheduler picks goroutines from this global queue and assigns them to available OS threads.
- Each OS thread (M) also has a local queue of goroutines. This local queue helps minimize contention and allows for efficient scheduling.
-
Work Stealing:
- The scheduler uses work-stealing techniques to balance the load across all available OS threads. If one OS thread has fewer tasks to execute, it may steal tasks from the local queue of another OS thread.
- This helps in achieving better utilization of resources and load balancing.
-
System Calls and Blocking Operations:
- When a goroutine makes a system call or blocks on an operation (e.g., I/O), the scheduler can use this opportunity to schedule other runnable goroutines on the same OS thread.
- The scheduler is designed to efficiently handle blocking operations without blocking the entire OS thread.
By managing the scheduling of goroutines and coordinating their execution across OS threads, the Go runtime provides a high-level concurrency model that is both efficient and easy for developers to work with. The scheduling decisions made by the runtime contribute to Go's ability to handle large numbers of concurrent tasks with relatively low overhead.
Goroutines: Creation
We can create a Goroutine simply by adding the go
prefix to a function invocation. This will tell Go to spin off what is called a Green thread and run the function in that green thread.
Most programming languages use Operating System threads (OS Threads), i.e. they have an individual function call stack dedicated to the execution of whatever code is handed into that thread. Now, traditionally these tend to be very large. They have for e.g., about 1 MB of RAM, they take quite a bit for the application to set up. And so you want to be very conservative about how you use threads. That's where you get into concepts like thread pooling, because the creation and destruction of threads is very expensive, thus we want to avoid that in most programming languages such as C#, Java.
In Go, we have a different model similar to the Erlang language and that is in the usage of the Green thread. So instead of creating these massive, heavy overhead threads, we're going to create an abstraction of a thread that we're going to call a Goroutine.
Inside the Go runtime, we've got a scheduler that;s going to map these Goroutines onto these Operating System threads for periods of time. The scheduler will then take turns with every CPU thread that's available and assign the different Goroutines, a certain amount of processing time, on those threads. But we don't have to interact with those low level threads directly, instead we interact with these high-level Goroutines.
The advantage of that is, since we have this abstraction, Goroutines can start with very, very small stack spaces, because they can be re-allocated very quickly. So they are very cheap to create and to destroy. So, it's not uncommon in a Go application to see 1000s or 10s of thousands of Goroutines running at the same time, and the application has no problem with that. Now, if you compare that to other languages that rely on Operating system threads that have 1 MB of overhead, there's no way you can run 10,000 threads in an environment like that.
Thus, by using Goroutines we get this lightweight abstraction over a thread and we no longer have to be afraid of creating and destroying them.
package main
import (
"fmt"
"time"
)
/** The main function executes in a Go routine itself */
func main() {
go sayHello() // This line will not have any time to execute on its own
time.Sleep(10) // Artificial delay of 10ms added to ensure execution completes before main exits. This is not a good practice and has been strictly used to demonstrate the point.
}
func sayHello() {
fmt.Println("Hello")
}
Using multiple Goroutines while using an anonymous function:
Note: In concurrent operations, the concurrent functions, i.e. Goroutines will return in no particular order.
package main
import (
"fmt"
"time"
)
func main() {
go sayHello()
go func() {
fmt.Println("My name is Jayanta")
}()
time.Sleep(10 * time.Millisecond)
}
func sayHello() {
fmt.Println("Hello")
}
Note: When the main function exits, all existing goroutines die. Be careful of any timers or abrupt ending to the main function that may kill goroutines prematurely.
Goroutines: Race Conditions when working with Goroutines and Closures
When working with Goroutines that are Closures, there are situations where we will encounter a race condition. This is a bad thing and something to be avoided.
In Go, function literals are closures: the implementation makes sure the variables referred to by the function survive as long as they are active.
A variable that is used within the closure if re-assigned again, after the Goroutine is declared, will still in most cases have the return value modified by the re-assignment. That is because,
most of the time, the Go scheduler is not going to interrupt the main thread until it hits the sleep call. Which means, even though it launches a Goroutine, it doesn't process it as its still executing the main
function. Hence the re-assignment occurs before the Goroutine has a chance to execute.
Hence, although we can access variables via the closure, it's generally not a good idea when working with Goroutines to access variables as closures. The way around is to pass the variable as a function argument, thus decoupling the variable from the Goroutine and this is the best practice when working with Goroutines.
package main
import (
"fmt"
"time"
)
func main() {
name := "Jayanta"
go sayHello() // Prints: "Hello"
/** Problematic way of working with Closures when Goroutines are involved */
go func() {
fmt.Printf("My name is %v\n", name) // Prints: "My name is Bhargav"
}()
/** Correct way of working with Closures when Goroutines are involved */
go func(name string) {
fmt.Printf("My name is %v\n", name) // Prints "My name is Jayanta"
}(name)
name = "Bhargav"
time.Sleep(10 * time.Millisecond)
}
func sayHello() {
fmt.Println("Hello")
}
Note: The order of the execution of the Goroutines is not guaranteed due to how concurrency works.
Now, this example above is still not how we want to run in production as sleep calls are a really bad practice as we are tying application performance to the real world clock and we have to continuously optimize the sleep time to the average performance time. So what are the alternatives?
One of the other alternatives we have is to use something called a Wait Group.
Goroutines: Synchronization
Wait Groups
The Wait Group can be accessed from the sync
package. What a Wait Group does is that, it's designed to synchronize multiple Goroutines together.
In the example below, we have 3 Goroutines:
- Goroutine executing the
main
function - Goroutine executing the
hello
function - Goroutine executing the anonymous function
Our course of action is to synchronize the main
function to the hello
Goroutine and the anonymous Goroutine. We can do that using Wait Groups.
The Wait Group works by incrementing a counter for every Goroutine and then decrementing the counter at the end of the execution of each Goroutine. We wait for the counter to turn zero and then exit the application.
Example of synchronization using WaitGroup:
package main
import (
"fmt"
"sync"
)
var wg = sync.WaitGroup {}
func main() {
name := "Jayanta"
// Increment the WaitGroup counter.
wg.Add(1)
go sayHello()
// Increment the WaitGroup counter.
wg.Add(1)
go func(name string) {
// Decrement the WaitGroup counter when the goroutine completes.
defer wg.Done()
fmt.Printf("My name is %v\n", name) // Prints "My name is Jayanta"
}(name)
name = "Bhargav"
// Wait blocks until the WaitGroup counter is zero. When zero, it exits.
wg.Wait()
}
func sayHello() {
// Decrement the WaitGroup counter when the goroutine completes.
defer wg.Done()
fmt.Println("Hello")
}
Note: WaitGroup is designed to be used concurrently. So multiple functions can call the
WaitGroup.Done
method without any problems.
Synchronization when having multiple Goroutines working on the same data:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
var counter = 0
func main() {
for i := 0; i < 10; i++ {
wg.Add(1)
go sayHello()
wg.Wait()
wg.Add(1)
go increment()
wg.Wait()
}
}
func sayHello() {
defer wg.Done()
fmt.Printf("Hello #%v\n", counter)
}
func increment() {
defer wg.Done()
counter++
}
/** Prints */
/*
Hello #0
Hello #1
Hello #2
Hello #3
Hello #4
Hello #5
Hello #6
Hello #7
Hello #8
Hello #9
*/
Mutexes
Just like WaitGroups, Mutexes are another way to handle the race conditions.
A Mutex is a mutual exclusion lock that the application has to honour. The zero value for a Mutex is an unlocked mutex.
A simple mutex is either locked or unlocked. If the mutex is locked and something tries to manipulate that value, it has to wait until the mutex is unlocked and they can obtain the mutex lock itself.
So what we can do with that is, we can protect parts of our code so that only one entity can be manipulating that code at a time. Typically what we use that for is to protect data to ensure that only thing can access the data at a single time.
With a RWMutex
, we can change things a little bit - as many things as want to, can read this data, but only one can write at a single time. Additionally, if anything is reading, then we cannot write to it at all and if anything is writing, then we can neither read nor write from it. Thus, with a RWMutex
we can have an infinite number of Readers but only one Writer. So if something makes a Write request, it has to wait until all the Read requests are done, and then the writer is going to lock the mutex, so nothing can read or write until the writer is done.
package main
import (
"fmt"
"sync"
)
var counter = 0
var m sync.RWMutex
func main() {
for i := 0; i < 10; i++ {
m.RLock()
go sayHello()
m.Lock()
go increment()
}
}
func sayHello() {
fmt.Printf("Hello #%v\n", counter)
m.RUnlock()
}
func increment() {
counter++
m.Unlock()
}
/** Prints */
/*
Hello #0
Hello #1
Hello #2
Hello #3
Hello #4
Hello #5
Hello #6
Hello #7
Hello #8
Hello #9
*/
Note: The problem with the above implementation of this counter example in both the WaitGroup example of this, and the Mutex is that we have completely destroyed concurrency and parallelism by running our code in a synchronous manner in a single threaded way. So any potential benefits from Goroutines are rendered defunct.
This is an example of when NOT to use Goroutines as we are better served running this with a single execution path and removing any concurrency. However, there are often situations, where you can get a significant advantage running tasks in parallel, and in those cases
WaitGroups
orMutexes
can be used to synchronize tasks together.
Goroutines: Parallelism
Parallelism is the ability to take the multiple tasks and subtasks within a single task and enable them to run at the same time on hardware with multiple computing resources, like multi-core processor, i.e. introduce parallelism to the application.
Manipulating the Number of Operating System Threads
By default, the number of Operating System threads available, correspond to the number of CPU Cores.
We can use the runtime.GOMAXPROCS
from the runtime
package to access and set the number of OS Threads.
-
runtime.GOMAXPROCS(-1)
: Returns the number of threads available. (Passing in a negative number, doesn't change the value and simply returns the current number of threads). If you are on a Quad-core system, by default,runtime.GOMAXPROCS(-1)
will showThreads: 4
. -
runtime.GOMAXPROCS(1)
: Set the number of OS threads to be 1. This can run our application in a single-threaded way. A general rule of thumb is to set this -
runtime.GOMAXPROCS(100)
: Valid. There is nothing stopping us from creating a massive number of Operating system threads.
Points to remember when working with threads in Go:
-
runtime.GOMAXPROCS
is a tuning variable for you to work with. The general best practice is 1 x Operating System thread per core is a minimum. -
A lot of times you will find that your application will get faster by increasing
runtime.GOMAXPROCS
beyond that value. -
An extremely high number like
100
will make us run into other problems like:- Additional memory overhead for maintaining 100 operating system threads
- Your scheduler has to work harder, because it has all these different threads to manage
Thus performance peaks, and then starts to fall back off, because your application is constantly rescheduling Goroutines on different threads and you're losing time whenever that occurs.
-
As your application gets closer to production, develop with
runtime.GOMAXPROCS
greater than 1, to reveal the race conditions as early as possible. -
Before you release to production, run your application through a performance test suite with varying values of
runtime.GOMAXPROCS
, to see what's the optimum number of threads for the best performance.
Goroutines: Best Practices
-
Don't create Goroutines in libraries. Let consumer control concurrency: Making a Library control concurrency can cause problems with synchronization and thus it is better to keep things single threaded and let the consumer decide when and when not to use a Goroutine. This advice can be softened a little bit, if you have a function call that's going to return a Channel that will return the result, then having a Goroutine in there might not be such a bad thing, because your consumer never really has to worry about how that unit of work is getting done. They don't really care if it's running concurrently or not because they are just going to be listening for a result on a channel.
-
When you create a Goroutine, know how it will end: If you don't have a way to stop a Goroutine, the Goroutine will continue forever and cause memory leaks. And slowly as the Goroutine ages, it could cause other issues and cause your application to crash.
-
Check for race conditions at compile time: We can simply check for race conditions by adding the
-race
flag to any go command we use for compiling likego run
,go build
,go install
, whatever you are using to get your application up and running. This will detect race conditions and report them on the console. E.g.go run -race src/Main.go
Channels
Most programming languages that are out there, were designed with a single processing core in mind. And so when concurrency and parallelism came into play, they were really kind of bolted onto the side and added as modifications. Thus, a lot of times, you're actually working with third-party libraries and packages in order to help with data synchronization and things like that.
Go, on the other hand was born in a multi-processor world. So it made sense as the language was being designed, to consider concurrency and parallelism from the beginning.
We already learned about Goroutines, how Go abstracts the concept of a thread into this higher concept called a Goroutine to allow hundreds or thousands or even tens of thousands of tasks to be going on in the application at the same time. In this section, we are going to take a look at channels and how those can be used to pass data between different Goroutines in a way that is safe, and prevents issues such as race conditions and memory sharing problems that can cause bugs in your application that are very difficult to debug.
Channels: Overview
When we are working with Channels in Go, we would almost always be working with them in context of Goroutines. The reason is that Channels are really designed to synchronize data transmission between multiple Goroutines.
Channels are created with the built in make
function.
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
ch := make(chan int)
wg.Add(2)
/** Receiving Goroutine */
go func() {
i := <- ch // Receive data from a channel
fmt.Println(i) // 40
wg.Done()
}()
/** Sending Goroutine */
go func() {
i := 40
ch <- i // Send data into a channel
i += 10
wg.Done()
}()
wg.Wait()
}
Another common use case for Goroutines is if you have data that is asynchronously processed, i.e. you may generate data quickly but it may take time to process it / it takes a long time to generate the data or have multiple generators that can be quickly processed. In that case, you might want to have a different number of Goroutines sending data into a channel than you are receiving.
Channels: Restrict Data Flow
A basic channel is a two-way street - we can send data in and get data out. But that's not always what you want to do with a channel. Sometimes you only want a send-only channel or a receive-only channel.
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
ch := make(chan int)
for j := 0; j < 5; j++ {
wg.Add(2)
/** Receiving Goroutine that accepts receive-only channel */
go func(ch <-chan int) {
i := <-ch // Receive data from a channel
fmt.Println(i)
wg.Done()
}(ch)
/** Sending Goroutine that accepts send-only Channel */
go func(ch chan<- int, j int) {
ch <- 40 * (j + 1) // Send data into a channel
wg.Done()
}(ch, j)
}
wg.Wait()
}
Note: Notice something unusual with this - the channel passed in each of the uni-directional channels is actually the bi-directional channel
ch
. This kind of feels like a polymorphic behaviour, but this is a special aspect of channels - the runtime understands this syntax. The runtime is actually going to cast this bi-directional channel into an uni-directional channel. But that's not something you can generally do in the Go language, this is something specific to channels.
Channels: Buffered Channels
Channels that have an internal data store so that they can store several messages at once just in case the sender and receiver aren't processing data at the same rate.
By default, when we are working with unbuffered channels, i.e. only one message can be in the channel at one time, any attempt to send any further messages, throws a deadlock error:
fatal error: all goroutines are asleep - deadlock!
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
ch := make(chan int)
/** Receiving Goroutine */
wg.Add(1)
go func() {
i := <- ch // Receive data from a channel
fmt.Println(i)
wg.Done()
}()
for j := 0; j < 5; j++ {
wg.Add(1)
/** Sending Goroutine */
go func() {
ch <- 40 // Send data into a channel
wg.Done()
}()
}
wg.Wait()
}
Providing a buffer to the make function:
func main() {
ch := make(chan int, 5)
/** Receiving Goroutine */
wg.Add(1)
go func() {
i := <-ch // Receive data from a channel
fmt.Println(i)
wg.Done()
}()
for j := 0; j < 5; j++ {
wg.Add(1)
/** Sending Goroutine */
go func(j int) {
ch <- 40 + j // Send data into a channel
wg.Done()
}(j)
wg.Wait()
}
wg.Wait()
}
Now, this might have solved one problem by getting rid of the deadlock error. But we are now losing information. While this isn't what the Buffer channels are intended to solve but it does have the internal store where we can store multiple messages back out.
What a Buffer Channel is designed to do is, if the sender or receiver operate at a different frequency than the other side. For e.g. we have a data ingesting system, and maybe we retrieve data from our sensors and a burst transmission - maybe we are acquiring data from seismometers. Maybe those seismometers, in order to save power, do not send data continuously, instead they send a burst transmission once an hour. So maybe in a burst transmission that lasts 5-6 seconds, we get an hours worth of data. So, in that case, our sender is going to be inundated with data when that burst happens and is going to have to have a way to deal with it. The receivers may take a while to process the data. So in that case, we might want to create a Buffer of these signals that are coming in from our seismometer, that's going to be able to accept that one hour's worth of data, and then our receivers can pull that data off, as they're able to process it and keep things working smoothly, so that the channel that's receiving the data from the sensors, doesn't get locked up by not being able to put the next message.
Thus, Buffer Channels help deadlock situations when a receiver needs some time to process the incoming channel data.
Channels: for...range
loops with channels
The way a for...range
loop works for slices for example is different than how it works for channels. If you have a slice with 5 elements, you are going to run through the for...range
loop, 5 times. There can be an infinite number of elements in a channel because you can constantly push a new message into it. So what is the way to signal a for...range
loop such that there are no new messages coming? The answer is, we need to close the channel using the built in close()
function and passing in the channel.
The for...range
loop for channels comes with the comma-ok
syntax instead of the index, value
that is the standard for slices or arrays.
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
ch := make(chan int)
wg.Add(2)
/** Receiving Goroutine that accepts receive-only channel */
go func(ch <-chan int) {
for i := range ch {
fmt.Println(i)
}
wg.Done()
}(ch)
/** Sending Goroutine that accepts send-only Channel */
go func(ch chan<- int) {
ch <- 40
ch <- 20
close(ch)
wg.Done()
}(ch)
wg.Wait()
}
With the comma-ok syntax:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
ch := make(chan int)
wg.Add(2)
/** Receiving Goroutine that accepts receive-only channel */
go func(ch <-chan int) {
/** With the comma ok syntax */
for {
if i, ok := <-ch; ok {
fmt.Println(i)
} else {
break
}
}
wg.Done()
}(ch)
/** Sending Goroutine that accepts send-only Channel */
go func(ch chan<- int) {
ch <- 40
ch <- 20
close(ch) // close the channel
wg.Done()
}(ch)
wg.Wait()
}
Note: Once a channel is closed it cannot be reopened. In fact, you cannot even detect if a channel is closed, except for by looking for the application panicking. Hence, you have to be careful when closing a channel as nothing else is going to be sending a message into it after that. So, if there's a possibility of that happening, the way to go about it is to use a deferred function and use a
recover
in there to recover from the panic that gets thrown.
Channels: Select Statements
There maybe situations where you can create Goroutines that don't have an obvious way to close. Remember, an application is shut down as soon as the last statement of the main function finishes execution. Everything is torn down and all resources are reclaimed as the Go runtime returns all the resources it was using, back to the Operating System. So, if there are any goroutines that have not exited gracefully, they will be torn down.
In some situations, that maybe acceptable. But in other situations, you might want to have more control over a Goroutine. It is considered a best practice to have a strategy for how your Goroutines will shut down as this can be a potential memory leak that can compromise the application.
One way to do this is to use a deferred function and close the channel when the main function exits.
Another way is to use Select Statements. Similar to switch
statements but specifically designed to work in the context of Channels.
Using a deferred function to close the channel:
package main
import (
"fmt"
"time"
)
const (
logInfo = "INFO"
logWarning = "WARNING"
logError = "ERROR"
)
type logEntry struct {
time time.Time
severity string
message string
}
var logCh = make(chan logEntry, 50)
func main() {
go logger()
defer func() { close(logCh) }() // Close the channel when the main function exits
logCh <- logEntry{time.Now(), logInfo, "App is starting"}
logCh <- logEntry{time.Now(), logInfo, "App is shutting down"}
time.Sleep(100 * time.Millisecond)
}
func logger() {
for entry := range logCh {
fmt.Printf("%v - [%v]%v\n", entry.time.Format("2006-01-02T15:04:05"), entry.severity, entry.message)
}
}
Using Select Statement to close the channel:
Note: A struct with no fields in the Go language is unique in that, it requires zero memory allocation. When setting up an empty struct as a channel, the idea is not to send data through it but the fact that a message was sent or received. That is all an empty struct can do as a channel. This is also called a signal-only channel. The benefit is that, there is zero memory allocation in sending the message, while having the ability to let the receiving side know that a message was sent.
package main
import (
"fmt"
"time"
)
const (
logInfo = "INFO"
logWarning = "WARNING"
logError = "ERROR"
)
type logEntry struct {
time time.Time
severity string
message string
}
var logCh = make(chan logEntry, 50)
var doneCh = make(chan struct{})
func main() {
go logger()
logCh <- logEntry{time.Now(), logInfo, "App is starting"}
logCh <- logEntry{time.Now(), logInfo, "App is shutting down"}
time.Sleep(100 * time.Millisecond)
doneCh <- struct{}{} // Pass an empty struct at the end of the main to signal the completion
}
func logger() {
LOOP:
for {
select {
case entry := <-logCh:
fmt.Printf("%v - [%v]%v\n", entry.time.Format("2006-01-02T15:04:05"), entry.severity, entry.message)
case <-doneCh:
break LOOP
}
}
}
Note: If you have a default case in the select statement, it no longer becomes a blocking select statement. What that does is, if there's a message ready on one of the channels that are being monitored, then it's going to execute that code path. If not, it will execute the default one. This is useful when you want to have a non-blocking select statement. The absence of the default case means, the select statement will block forever until a message does come in or the exit case (if any) is met.
Generics
Generics: Overview
Go 1.18 includes an implementation of generic features as described by the Type Parameters Proposal.
Generics allows you to write your code in a way where you don't have to repeat the same code.
With generics, you can declare and use functions
or types
that are written to work with any of a set of types provided by calling code.
Let's take a function that adds two numbers:
package main
import "fmt"
func Add(a, b int) int {
return a + b
}
func main() {
result := Add(1, 2)
fmt.Println("Result:", result)
}
This works fine as expected, however the problem is, we cannot pass anything except integers, particularly int32
integers. What if we want to pass floating point numbers or very large integers int64
or int128
?
We cannot do that in the current scenario as it will throw an error if we try it.
We may want to create a new AddFloat
function or an AddInt64
function and then detect then use those functions whenever we have to deal with those specific types. But you can see how we are repeating code here as the underlying operation is still an addition of a + b
. This is where Generics can be helpful.
With Generics the code can now be modified to look like this:
package main
import "fmt"
func Add[T int | float64 ](a, b T) T {
return a + b
}
func main() {
fmt.Println("Result:", Add(1, 2)) // Result: 3
fmt.Println("Result:", Add(1.1, 2.)) // Result: 3.1
}
This works and now we have the same function handling both int and float64 addition of two numbers.
However, there are many different kinds of integer types and floats. How do we deal with that? The answer is: Using interfaces
Generics: Using Interfaces
There are many different kinds of integer types and floats. We can add them in the function declaration like below:
func Add[T int8 | int16 | int | int64 | float32 | float64 ](a, b T) T {
return a + b
}
However, there are two shortcomings to the above approach:
- It's very verbose to write this.
- The whole idea is to not repeat and promote reusability of code. So if we need to create another Subtract function, we have to again repeat declaring all the number types like above.
There's a better way to do this and that is using Interfaces to declare the type:
type Number interface {
int8 | int16 | int | int64 | float32 | float64
}
func Add[T Number](a, b T) T {
return a + b
}
func Subtract[T Number](a, b T) T {
return a - b
}
Generics: Using constraints.Ordered
Although we have reusability in the above situation, it may still be a little verbose.
Simplification being one of the principles of the language, we need to import a package to simplify this even further.
The constraints
package comes with an Interface Ordered
accessible via constraints.Ordered
that is the union of all the number types and string (for string concatenation)
package main
import (
"fmt"
"golang.org/x/exp/constraints"
)
func Add[T constraints.Ordered](a, b T) T {
return a + b
}
func main() {
fmt.Println("Result:", Add(1, 2)) // Result: 3
fmt.Println("Result:", Add(1.1, 2.)) // Result: 3.1
}
Generics: Working with Type Aliases
Let's say we had a Type Alias declared and we want to use that in the function. We run that and we get an error that says the Type Alias doesn't satisfy the types. This is because the underlying type is not considered and the Type Alias is by default considered it's own type.
package main
import "fmt"
type Amount int
func Add[T int | float64 ](a, b T) T {
return a + b
}
func main() {
fmt.Println("Result:", Add(Amount(1), Amount(2))) // Amount does not satisfy int | float64 (possibly missing ~ for int in int | float64)
}
As you can see Go already provides an underlying suggestion to use the ~
character.
The tilde (~
) character specifies an approximation constraint ~T
that can be fulfilled by types that use T
as an underlying type.
Hence, we can now run the following successfully:
package main
import "fmt"
type Amount int
func Add[T ~int | float64](a, b T) T {
return a + b
}
func main() {
fmt.Println("Result:", Add(Amount(1), Amount(2))) // Result: 3
}
Generics: Example: Creating a mapping function
A mapping function as seen in many programming languages, iterates over a list and returns a new list with the elements mutated by a function. We want to use Generics to create a mapping function that can work across all number types.
First let's try to write the normal map function:
package main
import "fmt"
func Map(array []int, callback func(index, value int) int) []int {
var newArray []int
for index, value := range array {
newArray = append(newArray, callback(index, value))
}
return newArray
}
func main() {
var intArray = []int{1, 2, 3}
result := Map(intArray, func(i, n int) int {
return n * 2
})
fmt.Println(result)
}
Now, let's convert this into using Generics, so we can pass in any number type:
package main
import "fmt"
type Signed interface {
~int8 | ~int16 | ~int | ~int32 | ~int64
}
type Unsigned interface {
~uint8 | ~uint16 | ~uint | ~uint32 | ~uint64
}
type Float interface {
~float32 | ~float64
}
type Number interface {
Signed | Unsigned | Float
}
// Note: instead of the Number interface, we could use the `constraints.Ordered` interface
func Map[T Number](array []T, callback func(index int, value T) T) []T {
var newArray []T
for index, value := range array {
newArray = append(newArray, callback(index, value))
}
return newArray
}
func main() {
resultInt := Map([]int{1, 2, 3}, func(i, n int) int {
return n * 2
})
resultFloat := Map([]float32{1.1, 2.2, 3.3}, func(i int, n float32) float32 {
return n * 2
})
fmt.Println(resultInt) // [2, 4, 6]
fmt.Println(resultFloat) // [2.2, 4.4, 6.6]
}
Generics: Working with Structs
Let's say we are working with a User struct, that has one of the fields as an arbitrary Data type which we do not know for sure. We might be inclined to use interface {}
as follows:
package main
import "fmt"
type User struct {
ID int
Name string
Data interface {}
}
This is a bad idea as then the interface needs to be cast into the type we want it to be and the compiler can't help you.
Instead we can do the following:
package main
import (
"fmt"
"golang.org/x/exp/constraints"
)
type CustomData interface {
constraints.Ordered | []byte | []rune
}
type User[T CustomData] struct {
ID int
Name string
Data T
}
func main() {
u := User[string] {
ID: 123456,
Name: "John Doe",
Data: "Any CustomData type"
}
fmt.Println(u) // {123456 John Doe Any CustomData type}
}
Thus we can see using Generics with structs can give us an extra layer of flexibility in our data types, without sacrificing safety.
Generics: Working with Maps
The valid data types for the key of a Map in Go, is the type comparable
. A comparable
type is a umbrella for any type where you can compare one value to the other.
package main
import "fmt"
type CustomMap[T comparable, V int | string] map[T]V
func main() {
user := make(CustomMap[string, int])
user["id"] = 123456
fmt.Println(user) // map[id:123456]
}
Directives
go:embed
go:embed
is a directive introduced in Go 1.16 that allows you to embed files and directories directly into your Go source code. It simplifies the process of including static assets, configuration files, or other resources as part of your Go program binary. This is especially useful for creating self-contained binaries or distributing code with embedded assets.
Here's a basic overview of how go:embed
works:
-
Embedding Files: You can use
go:embed
to embed individual files or entire directories. For example, you can embed a single file like this://go:embed myfile.txt var fileContents string
Or, you can embed an entire directory like this:
//go:embed mydirectory/* var dirContents string
-
Accessing Embedded Files: The embedded content is treated as a string or byte slice in your Go code. You can access it like any other variable. For example:
fmt.Println(fileContents)
-
Build Process: When you build your Go program, the files or directories specified with
go:embed
are included in the binary as part of the program's data section. -
Read-Only: The embedded content is read-only, meaning you cannot modify it at runtime. It's designed for embedding static resources.
-
Benefits: Embedding assets in your binary simplifies deployment and distribution since you don't need to ship external files with your application. It also ensures that your program always has access to these resources, regardless of where it's executed.
Here's the link to the official Go documentation that provides more details and examples of using go:embed
:
The documentation includes usage examples and more advanced techniques for working with embedded files and directories in Go.
Standard Library
Some of the Core Packages and their methods are detailed below. These are the modules that come handy with everyday usage of the language:
Package | Description |
---|---|
fmt |
Formatted I/O with functions analogous to C's printf and scanf |
log |
Implements a simple logging package |
os |
A platform-independent interface to operating system functionality. |
io/fs |
Defines basic interfaces to a file system. |
math |
Provides basic constants and mathematical functions. |
strings |
Implements simple functions to manipulate UTF-8 encoded strings. |
strconv |
Implements conversions to and from string representations of basic data types. |
net/http |
Provides HTTP client and server implementations. |
html/template |
Implements data-driven templates for generating HTML output safe against code injection. |
encoding/json |
Implements encoding and decoding of JSON as defined in RFC 7159. |
time |
Provides functionality for measuring and displaying time. |
sync |
Provides basic synchronization primitives such as mutual exclusion (Mutex) locks and Wait Groups. |
flag |
Package flag implements command-line flag parsing. |
reflect |
Implements run-time reflection, allowing a program to manipulate objects with arbitrary types. |
regexp |
Implements regular expression search. |
fmt
Package fmt
implements formatted I/O with functions analogous to C's printf and scanf. The format 'verbs' are derived from C's but are simpler.
Verbs | Description |
---|---|
%v |
The value in a default format. When printing structs, the plus flag (%+v) adds field names |
%#v |
A Go-syntax representation of the value |
%T |
A Go-syntax representation of the type of the value |
%% |
A literal percent sign; consumes no value |
%t |
Boolean: The word true or false |
%d |
Integer: Base 10 |
%s |
The uninterpreted bytes of the string or slice |
%q |
A double-quoted string safely escaped with Go syntax |
%x |
base 16, with lower-case letters for a-f |
%X |
base 16, with upper-case letters for A-F |
More can be find on the Documentation Page for fmt
Methods:
Method | Function |
---|---|
fmt.Printf |
Prints a formatted string to the console |
fmt.Println |
Prints a new line |
fmt.Sprintf |
Returns a formatted string (doesn't print) |
fmt.Errorf |
Use formatting features to create descriptive error messages. |
Example 1: Basic Printing
package main
import "fmt"
type CustomMap[T comparable, V int | string] map[T]V
func main() {
user := make(CustomMap[string, int])
user["id"] = 123456
user["age"] = 32
fmt.Println(user) // map[id:123456]
fmt.Printf("The user's ID is: %v\n", user["id"]) // The user's ID is: 123456
fmt.Printf("The user's age is: %d\n", user["age"]) // The user's age is: 32
sprintfResult := fmt.Sprintf("The User: ID = %s, Age = %d\n", user["id"], user["age"])
fmt.Println(sprintfResult) // The User: ID = 123456, Age = 32
printError()
}
func printError() {
const name, id = "bueller", 17
err := fmt.Errorf("user %q (id %d) not found", name, id)
fmt.Println(err.Error())
}
log
Check log.go
for examples.
strings
Check StringOperations.go
for examples.
strconv
Check StringConversions.go
for examples.
encoding/json
Use Cases: Encoding a struct into JSON is often needed for sending back JSON response or storing a Log struct as JSON string.
package log
import (
"encoding/json"
)
type user struct{
Name string `json:"name"`
Age int8 `json:"age"`
IsActive bool `json:"is_active"`
}
func JSONOperations() {
/********************************************************************************************************************/
// JSON Encoding: Marshal
// JavaScript equivalent of: JSON.stringify
/********************************************************************************************************************/
var c customer = customer{
Name: "Jayanta Samaddar",
Age: 32,
IsActive: true,
}
bytes, err := json.Marshal(&c)
if err != nil {
log.Fatalln(err.Error())
} else {
fmt.Printf("Value: %v, Type: %T\n", string(bytes), string(bytes))
}
/********************************************************************************************************************/
// JSON Decoding: Unmarshal. Inverse of Marshal
// JavaScript equivalent of: JSON.parse
/********************************************************************************************************************/
var c2 customer
err = json.Unmarshal(bytes, &c2)
if err != nil {
log.Fatalln(err.Error())
} else {
c2.IsActive = false
// New c2 with changes
fmt.Printf("Value: %v, Type: %T\n", c2, c2)
// Original
fmt.Printf("Value: %v, Type: %T\n", c, c)
}
}
html/template
The html/template
package in Go provides a template engine for generating HTML and other text-based formats. It allows you to define templates with placeholders that you can fill with dynamic data when rendering. Here's an example of using the html/template
package along with explanations:
-
Import the "html/template" package:
import ( "html/template" "os" )
-
Define a template:
Create a template string with placeholders enclosed in double curly braces
{{ }}
. These placeholders will be replaced with dynamic data when rendering the template.const tmpl = ` <!DOCTYPE html> <html> <head> <title>{{ .Title }}</title> </head> <body> <h1>{{ .Heading }}</h1> <p>{{ .Content }}</p> </body> </html> `
If saving HTML templates as files to a directory, do have the files saved with the
.gohtml
extension and if you are using VS Code, use an extension that can syntax highlight the code. An example for such an extension is Gotemplate Syntax by casualjimYou can also access the values of a Map, if they are passed as data to the
Execute
method like this:const tmpl = ` <!DOCTYPE html> <html> <head> <title>{{ .Data "Title" }}</title> </head> <body> <h1>{{ .Data "Heading" }}</h1> <p>{{ .Data "Content" }}</p> </body> </html> `
-
Parse the template:
Use
template.New
to create a new template, and then use theParse
method to parse the template string.t, err := template.New("example").Parse(tmpl) if err != nil { panic(err) }
-
Prepare data:
Create a data structure (usually a Go struct) to hold the dynamic data that will be inserted into the template.
data := struct { Title string Heading string Content string }{ Title: "Sample Page", Heading: "Welcome to Go Templates", Content: "This is an example of using Go's HTML templates.", }
-
Execute the template:
Use the
Execute
method of the parsed template to fill in the placeholders with the data and write the result to an output stream (e.g., an HTTP response or a file).err = t.Execute(os.Stdout, data) if err != nil { panic(err) }
In this example, we're writing the rendered template to the standard output (
os.Stdout
), but you can use anyio.Writer
interface, such as an HTTP response writer or a file writer, depending on your use case. -
Output:
Running the code above will produce the following HTML output:
<!DOCTYPE html> <html> <head> <title>Sample Page</title> </head> <body> <h1>Welcome to Go Templates</h1> <p>This is an example of using Go's HTML templates.</p> </body> </html>
This is a basic example of using the "html/template" package to create and render an HTML template. You can create more complex templates with loops, conditionals, and custom functions to generate dynamic content for your web applications.
flag
Package flag implements command-line flag parsing.
Check flag.go for example usage.
reflect
Reflection in metaprogramming refers to an object's ability to introspect on itself and even modify itself.
Check ReflectionOperations.go
for examples.
References
Documentation ¶
There is no documentation for this package.