Hello World
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
Basic Data types
bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // alias for uint8
rune // alias for int32
// represents a Unicode code point
float32 float64
complex64 complex128
Declaring a varible
var message string = "Hello" // specifically assigning message as string and it is a variable.
message := "hello" // letting the compiler choose type of message according to whats on the right.
mileage, company := 8029, "Toyota"
// the above is similar to
mileage := 8029
company := "Toyota"
Type conversion
temperatureFloat := 88.26
temperatureInt := int64(temperatureFloat)
Conditional
if height > 6 {
fmt.Println("You are super tall!")
} else if height > 4 {
fmt.Println("You are tall enough!")
} else {
fmt.Println("You are not tall enough!")
}
if INITIAL_STATEMENT; CONDITION {
}
if length := getLength(email); length < 1 {
fmt.Println("Email is invalid")
}
Functions
func sub(x int, y int) int {
return x-y
}
Ignoring return values
func getPoint() (x int, y int) {
return 3, 4
}
// ignore y value
x, _ := getPoint()
Variadic functions
Many functions, especially those in std library, can take an abitrary number of final arguments. This is accomplished by using ”…” syntax in function signature.
func concat(strs ...string) string {
final := ""
// strs is just a slice of strings
for i := 0; i < len(strs); i++ {
final += strs[i]
}
return final
}
func main() {
final := concat("Hello ", "there ", "friend!")
fmt.Println(final)
// Output: Hello there friend!
}
fmt.Println and fmt.Sprintf both are variadic function as we can pass as many arguments in them as we want.
Structs
type car struct {
make string
model string
doors int
mileage int
frontWheel wheel
backWheel wheel
}
type wheel struct {
radius int
material string
}
myCar := car{}
myCar.frontWheel.radius = 5 // Use dot operator to access fields of a struct
Struct Methods
type rect struct {
width int
height int
}
// area has a receiver of (r rect)
// rect is the struct
// r is the placeholder
func (r rect) area() int {
return r.width * r.height
}
var r = rect{
width: 5,
height: 10,
}
fmt.Println(r.area())
// prints 50
Interfaces
Interface is defined as a set of method signatures.
type shape interface {
area() float64
perimeter() float64
}
type rect struct {
width, height float64
}
func (r rect) area() float64 {
return r.width * r.height
}
func (r rect) perimeter() float64 {
return 2*r.width + 2*r.height
}
type circle struct {
radius float64
}
func (c circle) area() float64 {
return math.Pi * c.radius * c.radius
}
func (c circle) perimeter() float64 {
return 2 * math.Pi * c.radius
}
Name your interface for better understanding==
type Copier interface {
Copy(string, string) int
}
type Copier interface { // This is easier to understand than the above one.
Copy(sourceFile string, destinationFile string) (bytesCopied int)
}
Type Assertions
A type assertion provides access to an interface value underlying concrete value.
t := i.(T) // This statement asserts that the interface value i holds the concrete type T and assigns the underlying T value to the variable t.
If i doesn’t hold a T, the statement will trigger a panic.
To test where an interface value holds a specific type, a type assertion can return two values. Value and Bool that tells if the assertion succeeded.
t, ok := i.(T)
If i holds a T, then t will be the underlying value and ok will be true and thus we can explicitly give an error out.
Type switches
A type switch is a construct that permits several type assertion in series.
switch v := i.(type) {
case T:
// here v has type T
case S:
// here v has type S
default:
// no match; here v has the same type as i
}
Some things to remember:
- Keep interface small and concise.
- Interfaces should have no knowledge of satisfying types.
- Interfaces are not classes.
Error
When something goes wrong inside a function, it should return a error value as it’s last param. The programmer can thus check it by comparing it to nil if the error occurred.
// Atoi converts a stringified number to an integer
i, err := strconv.Atoi("42b")
if err != nil {
fmt.Println("couldn't convert:", err)
// because "42b" isn't a valid integer, we print:
// couldn't convert: strconv.Atoi: parsing "42b": invalid syntax
// Note:
// 'parsing "42b": invalid syntax' is returned by the .Error() method
return
}
// if we get here, then
// i was converted successfully
Error interface
Because errors are interfaces, you can build your own custom types that implement the error interface.
type userError struct {
name string
}
func (e userError) Error() string {
return fmt.Sprintf("%v has a problem with their account", e.name)
}
func sendSMS(msg, userName string) error {
if !canSendToUser(userName) {
return userError{name: userName}
}
...
}
Error Package
The Go standard library has a "errors" package.
Loops
for INITIAL; CONDITION; AFTER{ // C-style loops without the parenthesis.
// do something
}
go doesn’t have any while loop
It’s just:
for CONDITION {
// do some stuff while CONDITION is true
}
Arrays
Same as C-arrays with type at the end instead of the start.
var myInts [10]int
primes := [6]int{2, 3, 5, 7, 11, 13}
Slices
Dynamically allocated array. Empty slice is nil. Slices always have an underlying array, though it isn’t always specified explicitly. To explicitly create a slice over an array, we do this:
primes := [6]int{2, 3, 5, 7, 11, 13}
mySlice := primes[1:4]
// mySlice = {3, 5, 7}
// arrayname[lowIndex:highIndex]
// arrayname[lowIndex:]
// arrayname[:highIndex]
// arrayname[:]
// lower index is inclusive but the high index is exclusive.
Make
Most of the time since we don’t need to think about the underlying array of the slice. We can create a new slice using the make function
// func make([]T, len, cap) []T
mySlice := make([]int, 5, 10)
// the capacity argument is usually omitted and defaults to the length
mySlice := make([]int, 5)
Spread Operator
The spread(…) operator allows us to pass a slice into a variadic function.
func printStrings(strings ...string) {
for i := 0; i < len(strings); i++ {
fmt.Println(strings[i])
}
}
func main() {
names := []string{"bob", "sue", "alice"}
printStrings(names...)
}
Append
func append(slice []Type, elems ...Type) []Type
You should append things to the same slice.
mySlice := []int{1, 2, 3}
mySlice = append(mySlice, 4)
Slices of slices - 2D matrix
rows := [][]int{}
Range based loop
Go provides syntactic sugar to iterate easily over elements of slice.
for INDEX, ELEMENT := range SLICE {
}
fruits := []string{"apple", "banana", "grape"}
for i, fruit := range fruits {
fmt.Println(i, fruit)
}
// 0 apple
// 1 banana
// 2 grape
Maps
Maps are like pythons dicts, javascript’s objects. In other sense key-value pairs. Hash map. Empty map is equal to nil.
ages := make(map[string]int)
ages["John"] = 37
ages["Mary"] = 24
ages["Mary"] = 21 // overwrites 24
ages = map[string]int{
"John": 37,
"Mary": 21,
}
Insert an element
m[key] = elem
Get an element
elem = m[key]
Delete an element
delete(m, key)
Check if key exists
elem, ok := m[key]
Nested maps
map[string]map[string]int
map[rune]map[string]int
map[int]map[string]map[string]int
Advanced Functions
Currying
Curryied function are those function with more than one argument that can wait for an argument unlike normal function that require all arguments before calling.
It allows a function with multiple arguments to be transformed into a sequence of functions, each taking a single argument.
Although proper currying is not possible in go we can simulate it.
add x = /y -> x + y currying function in haskell. add function wait for y and then proceed to give x + y.
To understand currying properly. refer to Currying.md
func main() {
squareFunc := selfMath(multiply)
doubleFunc := selfMath(add)
fmt.Println(squareFunc(5))
// prints 25
fmt.Println(doubleFunc(5))
// prints 10
}
func multiply(x, y int) int {
return x * y
}
func add(x, y int) int {
return x + y
}
func selfMath(mathFunc func(int, int) int) func (int) int {
return func(x int) int {
return mathFunc(x, x)
}
}
Defer
The defer keyword allows a function to be executed automatically just before its enclosing function returns.
Usually its for cleanup like closing files, releasing resources, etc when a function is finished executing.
func main() {
fmt.Println("Opening file...")
file, err := os.Open("example.txt")
if err != nil {
fmt.Println("Error opening file:", err)
return
}
defer file.Close() // Ensure the file is closed when main exits
fmt.Println("Reading file...")
// Assume some file reading operations here
fmt.Println("File reading completed.")
}
Closures
A closure is a function value that references variables from outside its body. The function may access and modify the variables within its scope even after the outer function has finished executing.
func concatter() func(string) string {
doc := ""
return func(word string) string {
doc += word + " "
return doc
}
}
func main() {
harryPotterAggregator := concatter()
harryPotterAggregator("Mr.")
harryPotterAggregator("and")
harryPotterAggregator("Mrs.")
harryPotterAggregator("Dursley")
harryPotterAggregator("of")
harryPotterAggregator("number")
harryPotterAggregator("four,")
harryPotterAggregator("Privet")
fmt.Println(harryPotterAggregator("Drive"))
// Mr. and Mrs. Dursley of number four, Privet Drive
}
Anonymous Functions
Anonymous function are those function that have no name. They are useful when defining a function that will only return once or while defining a quick closure.
// doMath accepts a function that converts one int into another
// and a slice of ints. It returns a slice of ints that have been
// converted by the passed in function.
func doMath(f func(int) int, nums []int) []int {
var results []int
for _, n := range nums {
results = append(results, f(n))
}
return results
}
func main() {
nums := []int{1, 2, 3, 4, 5}
// Here we define an anonymous function that doubles an int
// and pass it to doMath
allNumsDoubled := doMath(func(x int) int {
return x + x
}, nums)
fmt.Println(allNumsDoubled)
// prints:
// [2 4 6 8 10]
}
Pointers
Points to a memory address.
var p *int
myString := "hello"
myStringPtr := &myString
Go has no pointer arithmetic
Local Development
Packages
Every Go program is made up of packages. A package named main has an entrypoint at the main() function. A main package is being compiled into an executable programming.
When naming a Go package, it’s name is generally same as the name of the file. If i wanted to make a go package to create a random number. I can write package rnd but it’s preferred to write package rand.
A directory of a go code must have at most one package. All the .go file in a single directory must belong to one package.
Modules
A file name go.mod at root of the project declares the module. It contains:
- Module Path - It not only servers as an import path prefix for package within but also where the go command should look to download it. Like require github.com/google/examplepackage v1.3.0
- The version of Go.
- Any dependencies we use.
module github.com/wagslane/hellogo
go 1.22.1
// replace github.com/wagslane/mystrings v0.0.0 => ../mystrings // we write this to tell this module is at ../mustrings locally. This should be avoided. Just publish your code to a remote location. That's just how go creaters thought of handling packages and i think its better than npm at it.
require (
github.com/wagslane/mystrings v0.0.0
)
Channels
Concurrency
ypically, our code is executed one line at a time, one after the other. This is called sequential execution or synchronous execution. If the computer we’re running our code on has multiple cores, we can even execute multiple tasks at exactly the same time. If we’re running on a single core, a single core executes code at almost the same time by switching between tasks very quickly. Either way, the code we write looks the same in Go and takes advantage of whatever resources are available.
In go We just use the go keyword before an operation to make it concurrent. The go keyword spawns a new goroutine when calling a functions
Channels
Channels are typed, thread-safe queue. Channels allow different goroutines to communicate with each other.
ch := make(chan int) // make a channel.
ch <- 69 // send data to channel using the send operator.
v := <-ch // receive data from ch channel.
This reads and removes value from channel ch and saves it to v.
A deadlock is when a group of goroutines are all blocking so none of them can operate.This is a common bug that you need to watch out for in concurrent programming.
Empty structs are often used a tokens in Go programs. In this context, a token is a unary value. We don’t care what is passed through, we care if it is passed or not.
We can block and wait until something is sent on channel using
<-ch // this will block until it pops a single item off the channel, then continue, discarding an item.
Buffered Channels
Channels can optionally be buffered. We can provide a buffer length as the second argument to make() to create a buffered channel.
ch := make(chan int, 100)
A buffered channel only allows us to send data and only block channels when the buffer is full and receiving block only when the buffer is empty.
Closing Channels
Channels can be explicitly closed by a sender:
ch := make(chan int)
// do some stuff with the channel
close(ch)
We can check if a channel is closed
v, ok := <-ch
Range
Channels can be ranged over. In this the channel will receive the value over the channel (blocking at each iteration if nothing new is there) and will exit only when the channel is closed.
for item := range ch {
// item is the next value received from the channel
}
Select
Sometimes we have a single goroutine and we want to process the data in the order it comes in.
A select statement is used to listen to multiple channels at the same time.
select {
case i, ok := <- chInts:
fmt.Println(i)
case s, ok := <- chStrings:
fmt.Println(s)
default:
// receiving from ch would block
// so do something else
}
The default case in a select statement executes immediately if no other channel has a value ready.A default case stops the select statement from blocking.
Tickers
- time.Tick() returns a channel that sends a value on a given interval.
- time.After() sends a value once after the duration has passed.
- time.Sleep() blocks the current goroutine for specific amount of time.
Read only channels
A channel can be marked as read-only by casting it from a chan to a <-chan type.
func main() {
ch := make(chan int)
readCh(ch)
}
func readCh(ch <-chan int) {
// ch can only be read from
// in this function
}
Write only channels
We can similarly make then write only by shifting the arrow.
func writeCh(ch chan<- int) {
// ch can only be written to
// in this function
}
Extra Stuff
A send to a nil channel blocks forever
var c chan string // c is nil
c <- "let's get started" // blocks
A receive from a nil channel blocks forever
var c chan string // c is nil
fmt.Println(<-c) // blocks
A send to a closed channel panics
var c = make(chan int, 100)
close(c)
c <- 1 // panic: send on closed channel
A receive from a closed channel returns the zero value immediately
var c = make(chan int, 100)
close(c)
fmt.Println(<-c) // 0
Mutexes -
Mutually excludes different threads from accessing the same data at the same time. Mutexes allow us to lock access to data to control the access of data by goroutines.
Go std library provides a built-in implementation of a mutex with the sync.Mutex type and its two methods
func protected(){
mu.Lock() //
defer mu.Unlock() // use defer to ensure that we never forget to unlock.
// the rest of the function is protected
// any other calls to `mu.Lock()` will block
}
Maps are not thread-safe.
Map are not safe for concurrent use. If you have multiple go routines accessing the same map, at least one of them is writing to the map. We must lock the map with a mutex.
RW Mutex
The standard library also exposes a sync.RWMutex and it has these methods :
It can help with performance if we have read intensive task
Maps are cool with concurrent reads as we are not mutating data so more than one goroutines can read a map at the same time.
Generics
As go doesn’t have classes so it was impossible to reuse code that does the same thing.
As of Go v1.18, support for generics was released.
Type parameters
Put simply, generics allow us to use variables to refer to specific types.
func splitAnySlice[T any](s []T) ([]T, []T) {
mid := len(s)/2
return s[:mid], s[mid:]
}
Constraints
Constraints are just interfaces that allow us to write generics that only operate within the constraints of a given interface type.
type stringer interface {
String() string
}
func concat[T stringer](vals []T) string {
result := ""
for _, val := range vals {
// this is where the .String() method
// is used. That's why we need a more specific
// constraint instead of the any constraint
result += val.String()
}
return result
}
Interface type lists
We can now simply list a bunch of types to get a new interface/constraint.
// Ordered is a type constraint that matches any ordered type.
// An ordered type is one that supports the <, <=, >, and >= operators.
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~string
}
Generic type naming
func splitAnySlice[T any](s []T) ([]T, []T) {
mid := len(s)/2
return s[:mid], s[mid:]
}
T is just a variable name and can be anything. T has just become a convention like i for for loops.
func splitAnySlice[MyAnyType any](s []MyAnyType) ([]MyAnyType, []MyAnyType) {
mid := len(s)/2
return s[:mid], s[mid:]
}