Examples of Go Code
Structure a go project
Notes on Packages
http
These functions can accept any number of arguments
A function that accepts zero or more strings
printit()
printit("a","c","d")
func printit(mystrings ...string) {
if mystrings == nil {
fmt.Println("You need to enter something")
return
}
fmt.Println(mystrings)
}
A function can have one or more required parameters, and then an arbitary number of variadic
Below, only the num parameters is required.
printitX(5, "a", "c", "f")
// Can't pass a slice, so you need to unfurl the slice first
mySlice := []string{"a", "f", "k"}
printitX(2, mySlice) // This won't compile.
printitX(2, mySlice...) // Unfurl slice into individual arguments
func printitX(num int, mystrings ...string) {
if num <= 0 {
fmt.Println("You must enter a positive number for repeats")
return
}
if mystrings == nil {
fmt.Println("Can't do much if you don't include some strings")
}
for _, val := range mystrings {
for i := 0; i < num; i++ {
fmt.Println(val)
}
}
}
Types are fundamental to Go
You can create a type that just points to an existing type.
Below, myInt is based of int. It can only hold integer values, but it's still considered a seperate type
type myInt int
var i1 myInt = 42
var i2 int = 42 //standard built-int int type
// i1 += i2 //this won't work as int and myInt are still different types
i1 += myInt(i2)
fmt.Println(i1)
return
Seems pointless to create a new type of int isn't even compatible with the original ints unless values are converted.
There are a few reasons to do this:
eg, the time package for example has the type Duration which is just a type int64, but it has a bunch of time related methods attached to it.
Silly Example that shows you can add methods to types.
type myInt int
func (i myInt) countDouble() myInt {
// a counter that always double it's current value
return i * 2
}
func main() {
a := myInt(5)
b := myInt(7)
a = a.countDouble()
b = b.countDouble()
fmt.Println(a, b)
}
//
10 14
type person struct {
name string
age int
weightkg float64
}
Go's memory management works more efficiently if when creating a struct, you put the larger structures at the start of the struct.
In the 2nd example below, Go pads out the 8 bit numbers with another 8 bits to the fields are aligned in RAM.
# This is store more efficiently.
type stats struct {
Reach uint16
NumPosts uint8
NumLikes uint8
}
# This is stores less efficiently.
type stats struct {
NumPosts uint8
Reach uint16
NumLikes uint8
}
empty structs take up 0 bytes. They are the smallest possible type in Go.
They are used often in maps and channels, but I haven't figured out why yet.
The reason an empty anonmous struct has two pairs of braces is:
struct{} is the type. ie empty struct
{} is the value. empty struct literal
// anonymous empty struct type
empty := struct{}{}
// named empty struct type
type emptyStruct struct{}
empty := emptyStruct{}
Say you wanted to embed an adddres type to the above person struct
You can embed it as named fields or anonymously
Eg, below.
Employee embeds address as named fields
Person embeds address anonymously
type address struct {
street string
suburb string
postcode string
}
type person struct {
name string
age int
weightkg float64
street int
address
}
type employee struct {
name string
age int
weightkg float64
street int
homeAddress address
}
With named fields, you have to access them all explicitly
With anonymous fields, they get promoted to the outer struct, so can be access from there.
// Employee uses named fields
x := employee.homeAddress.suburb // is valid
x := employee.suburb // NOT valid
// Person uses anonymous fields
x := person.address.suburb // is valid
x := person.suburb // is also valid
// Both person and address have a field called street, so to access the street field in the embedded address struct, it needs to be called explicilty
x := person.street // retreives the street field in the person struct
x := person.address.street // retreives the street in address
If you embed a struct anonymously, it can be accessed at the top level, but you need to specify the embedded struct when using a composite literal
type car struct {
brand string
model string
}
type truck struct {
// "car" is embedded, so the definition of a
// "truck" now also additionally contains all
// of the fields of the car struct
car
bedSize int
}
lanesTruck := truck{
bedSize: 10,
car: car{
brand: "Toyota",
model: "Tundra",
},
}
fmt.Println(lanesTruck.brand) // Toyota
fmt.Println(lanesTruck.model) // Tundra
Being unicode, you can't get the number of characters in a string with len(string) as len returns the number of bytes, not the number of characters (Runes)
Non ascii characters use more than 1 byte.
str := "Gagf"
fmt.Println("String", str)
fmt.Println("Byte length", len(str))
fmt.Println("# of Characters:", utf8.RuneCountInString(str))
// output
Gagf
4
4
str := "Gagf😀💋"
fmt.Println("String", str)
fmt.Println("Byte length", len(str))
fmt.Println("# of Characters:", utf8.RuneCountInString(str))
// Output
Gagf😀💋
12
6
A map is an unordered list of key/value pairs
Maps are different to other types in that they are effectively passed by reference to functions
The value of a map is nil if empty
An attempt to read a non-existent key will return the zero value of that type
You can read a nil map, but an attempt to write to one will cause a panic.
That is why you should create a map with make. Make can create an empty map.
An empty map is equivalent to a nil map, except that items can be added to an empty map, but not a nil one.
The optional make function returns an empty map of the give type that it initialised.
myMap := make(map[string]int)
myMap := make(map[string],int, 100)
//map where the key is a string
//value is a another map with a key and value of string
mybigMap := make(map[string]map[string]string,20)
The key of a map must ber a comparable type
which rules out using functions, maps or slices as keys.
Insert or update an element
map[key] = "testes"
// If map value is a number
map[key]++
map[key]--
Retreive a value
a := map[key]
Retrieving a key that doesn't exit just retreives the zero value of the type
To tell if a key actually exists in the map, you need to check the second variable return by the map function which is boolean. true if the key exists. false if not.
if value, ok := map[69]; ok {
fmt.Println("Key Exists. It is: ", value)
} else {
fmt.Println("Key doesn't exist")
}
Deleting a key
delete (map, key)
Map Literal
z := map[int]string //this won't work
z := map[int]string{} //This will work
z := map[int]string {
10: "gagf",
69: "me",
3: "you",
}
type Vertex struct {
Lat, Long float64
}
var m = map[string]Vertex{
"Bell Labs": Vertex{40.68433, -74.39967},
"Google": Vertex{37.42202, -122.08408},
}
If the type is clear from the context, you can omit the type in the literal.
In the above, it's been defined that they value is of type Vertex when creating the map.
You therefore don't need to include Vertex on each literal and so it can be shortened to below:
var m = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},
"Google": {37.42202, -122.08408},
}
Using a field of a struct as a key to the entire struct.
In this example, there is a struct of person that has a field called last.
Their last name is used as a map key that then points to the entire struct record as the value
type person struct {
first string
last string
flavors []string
}
func main() {
me := person{
first: "Craigus",
last: "Hammondus",
flavors: []string{"chocolate", "mint choc", "Choc Chip", "baileys"},
}
you := person{
first: "Lily",
last: "Liu",
flavors: []string{"cookie dough", "not mint"},
}
m := map[string]person{
me.last: me,
you.last: you,
}
// range over each key in the map.
for _, v := range m {
fmt.Printf("Key: %v\n", v.last)
// range over each flavour for that person
for _, v2 := range v.flavors {
fmt.Println(v2)
}
}
The value of a map can itself be a map.
eg, we have a map where the key is the symbol for an atom element.
H = helium, Li = Lithium
For each value, there is another map that contains the elements name and state
elements := map[string]map[string]string{
"H": map[string]string{ // specifying the type here is redundant
"name": "Hydrogen",
"state": "gas",
},
"He": {
"name": "Heilium",
"state": "gas",
},
"Li": {
"name": "Lithium",
"state": "solid",
},
}
fmt.Println(elements["H"])
fmt.Println(elements["H"]["name"])
fmt.Println(elements["Li"]["state"])
//
map[name:Hydrogen state:gas]
Hydrogen
solid
Setting a map literal is similar to many other go literals.
var timeZone = map[string]int {
"UTC": 0,
"EST": -5*60*60,
"AEST": 10*60*60,
}
ie, you want to track attendance.
If you look up a name that doesn't exist, you will get the zero value for bool which is false.
attended := map[string]bool {
"Craigus": true,
"Dickus": true,
...
}
if attended[person] {
fmt.Println(person, "was at the meeting")
}
An empty struct doesn't take up any space in memory
Create any empty struct to indicate true.
Use the comma,ok idiom to check if they exist.
attended := map[string]struct{}{
"Ann": {},
"Joe": {},
...
}
if _, ok := attended[person]; ok {
fmt.Println(person, "was at the meeting")
}
A setter method a used to set fields or other values within a defined type.
A use for this would be to check that the data is valid.
Used to retreive fields from within a defined type.
Getter methods don't need to receive a pointer and could just use the variable directly, but convention is that if the setter received a pointer, then the getter should as well.
A use for this is when the fields are private.
To make a struct public, but a field private, the struct name starts with a capital letter, but the fields are lowercase. The struct also needs to be in a seperate package and imported into main
type Date struct {
year int
month int
day int
}
// Setters
func (d *Date) SetMonth(month int) error {
if month < 1 || month > 12 {
return errors.New("invalid month")
}
d.month = month
return nil
}
// Getters
func (d *Date) Month() int {
return d.month
}
By Convention Setter use Set followed by the name of the variable it accesses.
Getters just use the name of the variable they are accessing so:
SetYear for a setter
Year for a getter
Normal or "Concrete" types specify the underlying types that hold their data and what it's values can do (ie, the methods it has)
Interface types only describe the signature of its methods.
The signature is what types of data it's methods will accept, and what types of data it will return.
Any type that satifies that signature fully is automatically attached to that interface.
Types can have additional methods over and above what is required to be part of an interface, but when type is used via an interface, only the methods described in the interface can be used.
Interfaces can help with code duplication
The following interface is legal code, but the intent isn't that clear
type Copier interface {
Copy(string, string) int
}
// We can name the parameters to make the code more readable for humans
type Copier interface {
Copy(sourceFile string, destinationFile string) (bytesCopied int)
}
When working with interfaces, it can be handy sometime to access the underlying type of an interface value. You can cant an interface to its underlying type using a type assertion.
The below example.
There is a share interface, and the circle struct implememnts the shape interface.
We want to check if shape s is also a circle
Using c, ok := s.(circle) We try to create circle c from shape s by asserting it as a circle.
If that assertion is successful, then shape s was also a circle
type shape interface {
area() float64
}
type circle struct {
radius float64
}
c, ok := s.(circle)
if !ok {
// log an error if s isn't a circle
log.Fatal("s is not a circle")
}
radius := c.radius
A type switch makes it easy to do several type assertions in a series.
It is similar to a regular switch statement, but the cases specify types instead of values
func printNumericValue(num interface{}) {
switch v := num.(type) {
case int:
fmt.Printf("%T\n", v)
case string:
fmt.Printf("%T\n", v)
default:
fmt.Printf("%T\n", v)
}
}
func main() {
printNumericValue(1)
// prints "int"
printNumericValue("1")
// prints "string"
printNumericValue(struct{}{})
// prints "struct {}"
}
The fmt package has the stringer interface which allows you define on any type how it will be displayed when printed.
Print, Println and Printf use the stringer interface as do the similar function like Sprintf
You just need a method called String() that returns a string to satisfy this interface.
type Stringer interface {
String() string
}
This simple example makes the default method of printing a float number have only 2 decimal points
func main() {
n1, n2 := 0.1, 0.2
normalfloat := n1 + n2
var myfloat niceFloat = niceFloat(n1 + n2)
fmt.Println("Normal float output: ", normalfloat)
fmt.Printf("Manually limiting it: %0.2f\n", normalfloat)
fmt.Print("Using the custom type: ", myfloat, "\n")
xx := fmt.Sprintln(myfloat)
fmt.Println("Same using Sprintf: ", xx)
}
type niceFloat float64
func (f niceFloat) String() string {
return fmt.Sprintf("%0.2f", f)
}
// Output
Normal float output: 0.30000000000000004
Manually limiting it: 0.30
Using the custom type: 0.30
Same using Sprintf: 0.30
To pass flags to a Go program with defaults.
addr := flag.String("addr", ":4000", "HTTP Listen Address:Port")
// or
type config struct {
addr string
statidDr string
}
var cfg config
flag.StringVar(&cfg.addr, "addr", ":4000", "HTTP Listen Address:Port")
flag.StringVar(&cfg.staticDir,"static-dir","./ui/static","Path to static assets")
flag.Parse()
//flag.StringVar can be IntVar, BoolVar etc
Have to start with a letter, then any number of letters and numbers
If it starts with a capital letter, it can be exported to other packages
Conventions are to use camelCase.
Outside of a function, variables have to be declared with the long form using the var keyword
Inside a function, it can be define with var or with the short form.
// Long form. Uses var. can be used in or outside a function
var a int // declares with types zero value
var a int = 1
var (
a = 1
b = 2
c = 3
)
// Short form. Can only be used inside a function
a := 1
a,b := 1, 5.5
Zero values
If variable declared without setting a value, it is set to it's zero value
Variables defined in a function are normally scoped to that function, and so when that function completes, those variables are removed from memory.
If a function creates a variable and returns a pointer to that variable, that variable will be allocated on the heap rather than the stack, and so that memory will be retained, and the variable usable, after the function ends
func createInt() *int {
x := 42 // Local variable (escapes to the heap)
return &x // Safe: x lives on
}
func main() {
ptr := createInt()
fmt.Println(*ptr) // Works: x is still accessible
}
A closure is a function that captures variables from it surrounding scope, and those variables persist after that function has finished executing.
The classic closure is to create a counter that remembers where it is up to between calls
func counter() func() int {
count := 5
return func() int {
count++
return count
}
}
func main() {
increment := counter()
anotherinc := counter()
fmt.Println(increment()) // 6
fmt.Println(increment()) // 7
fmt.Println(anotherinc()) // 6
fmt.Println(increment()) // 8
fmt.Println(anotherinc()) // 7
}
The normal scope for variables is Block Scope
Within a function, you can surround code within curly braces independant of any other statements such as loops for if blocks and any variables declared within that block is only usable in that block (or any nested blocks)
func main() {
{
testes := "gagf"
fmt.Println(testes) //will work
{
fmt.Println(testes) // will work
}
}
fmt.Println(testes) //WON'T WORK
}
Variables defined within a for or if block are local to that block
for x := 3; x < 10; x++ {
fmt.Println(x)
}
fmt.Println(x) // this doesn't compile.
//
if count := 5; count > 4 {
fmt.Println(count)
}
fmt.Println(count) //won't work
Packages and modules are both ways of organsing code.
A package is made up of Go files in a single directory that all have the same package statement at the beginning.
All the go files that make up a package need to be in the same directory.
All go files in one directory have to be the same package. You can't have multiple packages share a directory.
All go files in the same directory, and therefore the same package can automatically see all functions and variables in all other files in that package
If the project has multiple packages, put the other packages in subdirectoires with the same name as the package
Create a directory for a module
Convention is to give the directory the same name as the module
go mod init <modulename> which will just create a go.mod file
Say you were developing a library module that is going to live on github, but it's not there yet. Until you do get it up on a remote repo, you can still access it locally by doing something similar to this. mystrings will eventually be on github, but just for some initial development, you can override it such that is just located locally at ../mystrings
module github.com/hammondus/hellogo
go 1.25.1
replace github.com/hammondus/mystrings v0.0.0 => ../mystrings
require github.com/hammondus/mystrings v0.0.0
Arrays are the basic building blocks of slices. They are fixed in size, and on creation, all elements are set to the zero value for that type.
Slices are built on arrays. They provide an abstraction that takes away some of the limits of arrays. A slice is a pointer to an array that contains 3 components.
Array and Slices differ on how they are passed.
If you pass an array to a function, it is passed by value (copied)
As a slice is a pointer, the pointer is passed, so it is effectively passed by reference.
var primes [5]int defines an array of 5 integer elements. The [5] is part of the arrays type and so the array's size cannot change.
Can use Array literal to populate the elements.
var primes [5]int = [5]int{1, 3, 5, 7}
// or
primes := [5]int{1, 3, 5, 7}
fmt.Println(primes)
[1 3 5 7 0]
Literals can go over multiple lines
text := [4]string{
"This is a series",
"of strings which",
"would be messy to put",
"all on one line of text",
}
Can use a simple for loop, but if you go out of bounds, the program will panic
primes := [5]int{1, 3, 5}
// This will panic
for i := 0; i < 6; i++ {
fmt.Println(primes[i])
}
//This is a better way
for i := 0; i < len(primes); i++ {
fmt.Println(primes[i])
}
// Using the modern and best method
for i, value := range primes {
fmt.Printf("index: %v\tvalue: %v\n", i, value)
}
index: 0 value: 1
index: 1 value: 3
...
// if you don't need the index, use a blank identifier.
for _, value := range primes {
fmt.Println(value)
}
1
3
...
Defining a slice is similar to an array except you don't include the number of elements
You can also define a slice that points to an existing array.
var myArray [5]string // an array with 5 elements
var mySlice []string // a slice with no size limit
// Use make to define a slice with a length of 7 and a capacity of 10
cocko := make([]int, 7, 10)
If you define a slice with a literal or with make, you can only access the underlying array via the slice.
If you have an existng array, you can use the slice operator to define a slice that points to the existing array. An array can have multiple (and even overlapping) slices that point to it and you can still access the underlying array directly.
As slices are just a pointer, any changes to slice are reflected on the array, and vice versa.
// define a slice from index 1 to index 2 of myArray
// start slice at index 1. End slice BEFORE index 3
// ie, slice goes from index 1 to 2
var myArray [10]int = [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
slice1 := myArray[1:3] // slice points to indexes 1 -> 2
slice2 := myArray[2:6] // slice points to indexes 2 -> 5
slice3 := myArray[:4] // slice points to index 0 -> 3
slice4 := myArray[5:] // slice points to index 5 -> 9
fmt.Println(myArray)
fmt.Println("slice1", slice1)
fmt.Println("slice2", slice2)
fmt.Println("slice3", slice3)
fmt.Println("slice4", slice4)
slice1 [1 2]
slice2 [2 3 4 5]
slice3 [0 1 2 3]
slice4 [5 6 7 8 9]
if you append to a slice, you should store the result into the same slice
Appending adds 1 to the length. If you reach the capacity, the a new underlying array is created which is double the exising length.
os.Args is a slice of strings represending all the command line arguments used when invoking the program. The first item in the slice is the name used to invoke the program itself. This first item include the full path used to invoke the program, ie, testes or ./testes or ./dev/go/testes depending on how it was invoked from the commmand line
Super simple http client using the http.DefaultClient
The http.DefaultClient isn't recommended in prod code. For one, it has a timeout(0), so the request doesn't time out.
func getIssueData(url string) ([]byte, error) {
res, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("error creating request: %w", err)
}
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("error reading response: %w", err)
}
return data, nil
}
Routes that don't end in a slash will need an exact match
Route than do end in a slash, the slash will act like a * wildcard
mux.HandlFunc("/testes",x) - will only match /testes
...("/testes/") - is like /testes/*. ie, match all after testes
{\$} - Stop wildcard matching
...("/") - will match everything
...("/{$}") - will only match /
If you have wildcards, each segment between slashes can only have one wildcard, and the wildcard needs to fill the whole segment
Valid
...("/products/{category}/item/{itemid}")
Both category and date not valid
...("/products/c_{category}/date/{m}{d}{y}")
In your handler, retrieve the wildcard value with `r.PathValue()
category := r.PathValue("category")
id := r.PathValue("itemid")
It is always returned as a string, and it not sanitised, so needs to be validated by your code.
Wildcards normally match a single segment of the requeest.
This can be extended to subsequent segments
...("/post/{path}")
Will match on one path
http://post/mypath - will work
http://post/mypath/another - will not work
However
...("/post/{path...}") will match on both
By default, every response from a handler sends the http status code 200 OK as well as 3 system generated headers. Date, Content-Length and Content-Type
eg:
HTTP/1.1 200 OK
Date: Sun, 17 Aug 2025 01:10:59 GMT
Content-Length: 20
Content-Type: text/plain; charset=utf-8
To change for example to a http response of 201 Created
w.WriteHeader(201)
You can only call WriteHeader once, and it has to be done before any call to w.Write()
The net/http package provides http status code constants to use instead of numbers
w.WriteHeader(http.StatusCreated)
Any changes to the header need to be made before you call w.WriteHeader or w.Write
Go sniffs the response body to get content type, but it can't distinguish between text and json, so json needs to be explicitly set
`w.Header().Set("Content-Type","application/json")
.Header().Add() Appends a new header
.Header().Set() Creates a new header. Overwrites if it exists
.Header().Del() Deletes a header
.Header().Get() Retrieves the first vallue of a header
.Header().Values() Retrieves a slice of all values of a header
Headers will automatically be converts using textproto.CanonicalMIMEHeaderKey() so that the first character, or a character following a hyphen, is uppercase followed by lower case
If you need to avoid this, use can edit the underlying header map directly which has the type map[string][]string
w.Header()["X-XSS-Protection"] = []string{"1; mode=block"}
If http/2 is used, all header names and values are converted to lowercase as per the http2 spec.
Put in comments just before the package or function name to document them.
Package following by its namegodoc -http=localhost:6060
The below method that is supposed to double any value each time it's called wont work due to arguments getting passed by value. x won't change
type Number int
func (n Number) Double() {
n *= 2
}
func main() {
x := Number(4)
x.Double()
fmt.Println(x)
}
//
4
Convert the method to use pointers, and it does work.
No need to change the type or the code that calls the method. Just the method itself
func (n *Number) Double() {
*n *= 2
}
When using receivers with pointers, you don't need to send the address of the struct to the receiver, while you do if using a normal function.
Compare lines 17 which calls the setColor methods with line 24 which just calls a standard function
type car struct {
color string
}
func (c *car) setColor(color string) {
c.color = color
}
func red(mycar *car) {
mycar.color = "red"
}
func main() {
c := car{
color: "white",
}
c.setColor("blue")
// When using a receiver with a pointer, no need to send the address of the struct.
fmt.Println(c.color)
// prints "blue"
// Using a normal function, you have to send the address of the struct
red(&c)
fmt.Println(c.color)
}
You can create errors with either
err := errors.New("file not found")
err := fmt.Errorf("file not found")
err := fmt.Errorf("%d files not found: %w", attempts, originalErr)
errors.New("string") are good for simple static error messages, and uses less resources.
fmt.Errorf(".....") allows printf style formatting for more dynamic error messages. Takes up slight more resources. It also also error wrapping with the %w verb.
The error type is really just an interface.
type error interface {
Error() string
}
If a type has an Error method that returns a string, it satifies the error interface, and it's an error value. This means you can define your own types and use them anywhere an error value is required.
// Example
func main() {
err := checkTemp(6.0, 5.1)
if err != nil {
log.Println(err)
}
}
type OverheatError float64
func (o OverheatError) Error() string {
return fmt.Sprintf("Overheating by %0.2f degrees", o)
}
func checkTemp(actual float64, safe float64) error {
excess := actual - safe
if excess > 0 {
return OverheatError(excess)
}
return nil
}
You can Defer a function to run automatically when the function that contains the Defer ends. Typically used to close a file after opening it. That way, regardless of how you exit the function, the file will get closed.
If a program panics, deferred functions are still executed.
file, err := os.Open("votes.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
Filename has to end in _test.go
When you run tests with go test <modulename>, only functions that start with the name Test, so if you have some helper functions just don't start their name with Test and they won't run automatically.
go test testes run all the tests.
go test testes -v verbose
go test testes -run Two only run tests with the word Two in the name
There are a few forms of the comma ok idiom.
v, ok := names["cocko"]
if !ok {
//name wasn't in map
}
if _, ok := names["cocko"]; !ok {
//name wasn't in map
}
You need to make a channel before you can use it, and it needs a type.
ch := make(chan int)
A sender will block until the data is received at the other end.
A receiver will block until it gets data. Once it receives it, it deletes that data from the channel.
If you don't care about the data, but are just using the channel as a signal, you can receive to nothing
// Sent data on a channel
ch <- 69
// Receive data on a channel
v := <-ch
fmt.Println(<-ch)
You may wish to use channels to send a signal to do something, but there is no data required to be transferred. In that case, send an empty struct as an empty struct uses 0 bytes.
func getDBsChannel(numDBs int) (chan struct{}, *int) {
count := 0
ch := make(chan struct{})
go func() {
for i := 0; i < numDBs; i++ {
ch <- struct{}{}
fmt.Printf("Database %v is online\n", i+1)
count++
}
}()
return ch, &count
}
func waitForDBs(numDBs int, dbChan chan struct{}) {
for i := 0; i < numDBs; i++ {
<-dbChan
}
}
Sending on a closed channel will cause a panic
Closing an already closed channel will cause a panic
Receiving on a closed channel will immediately return the zero value for type on the channel
use the comma ok idion to check for closed channel
func countReports(numSentCh chan int) int {
count := 0
for {
v, ok := <- numSentCh
if !ok {
return count
}
count += v
}
return count
}
This will receive values over a channel until and exit when the channel is closed.
for item := range ch {
//do shit
}
You may want to listen on multiple channels and process them in whatever order the data arrives.
If multiple channels are ready at the same time, the order will be chosen randomaly
You use the select statement to do that.
select {
case i, ok := <- chInts:
fmt.Println(i)
case s, ok := <- chStrings:
fmt.Println(s)
default:
// if no channel has a value, the select statement will block, UNLESS
there is a default case, which will execute immediately if no channel is ready with a value
}
If you don't need the data in the select statement, but are just using it for signalling
select {
case <- channel1:
//do stuff
case <- channel2:
// do other stuff
default:
// stop select from blocking.
// Do something else again if no signals
}
time.Tick() returns a channel that sends a value on a given interval
time.After() sends a value once the duration has passed
time.Sleep() blocks the current goroutine for the specified duration
These functions all take time.Duration
eg
time.Tick(500 * time.Millisecond)
time.After(2 * time.Second)
func readWrite(ch chan int) // normal read write
func readCh(ch <- chan int) // in this function, read only
func writeCh(ch chan<- int) // in this function, write only
Mutexes allows you to lock access to data. This ensures we can control which go routines can access certain data at which time.
func protected() {
mu.Lock()
defer mu.Unlock()
// rest of the function is protected.
// any other calls to mu.Lock() will block
}
If is safe to read threads simultaneously safely.
In a read intensive process, use RLock() and RUnlock() for the components that only read. It will allow the reads to run concurrently, but will still block successfully if another process uses Lock() when it writes.
If you have multiple go routines that are accessing the same map, and at least one of them is writing to the map, you much lock the write with a mutex.
Maps are safe for read/read concurrency, but not read/write or write/write
First, an example that is not thread safe and will crash if run on a multi-core machine
Followed by the same code using mutexes to be safe
// unsafe version
package main
import (
"fmt"
)
func main() {
m := map[int]int{}
go writeLoop(m)
go readLoop(m)
// stop program from exiting, must be killed
block := make(chan struct{})
<-block
}
func writeLoop(m map[int]int) {
for {
for i := 0; i < 100; i++ {
m[i] = i
}
}
}
func readLoop(m map[int]int) {
for {
for k, v := range m {
fmt.Println(k, "-", v)
}
}
}
//
// Safe version using mutexes
package main
import (
"fmt"
"sync"
)
func main() {
m := map[int]int{}
mu := &sync.Mutex{}
go writeLoop(m, mu)
go readLoop(m, mu)
// stop program from exiting, must be killed
block := make(chan struct{})
<-block
}
func writeLoop(m map[int]int, mu *sync.Mutex) {
for {
for i := 0; i < 100; i++ {
mu.Lock()
m[i] = i
mu.Unlock()
}
}
}
func readLoop(m map[int]int, mu *sync.Mutex) {
for {
mu.Lock()
for k, v := range m {
fmt.Println(k, "-", v)
}
mu.Unlock()
}
}
Allows functions to accept parameters of different types.
You define the Types you will accept in square brackets after the function name, but before the list of arguments.
Simple example.
You have a function that splits a slice evenly into two slices
Without generics, you would need one for each type.
With generics, you de
func splitIntSlice(s []int) ([]int, []int) {
mid := len(s)/2
return s[:mid], s[mid:]
}
func splitStringSlice(s []string) ([]string, []string) {
mid := len(s)/2
return s[:mid], s[mid:]
}
// With Generics, this can be combined into one
func splitAnySlice[T any](s []T) ([]T, []T) {
mid := len(s)/2
return s[:mid], s[mid:]
}
If you want to initial a variable to the zero value of a variable with generics
func getLast[T any](s []T) T {
//create a variable of the passed T, but of zero value for that type
var zero T
if len(s) == 0 {
return zero
}
return s[len(s)-1]
}
To limit, or constraint the types used in generic, use interfaces
func chargeLineItem[T lineItem](newItem T, oldItems []T) ([]T, error) {}
type lineItem interface {
GetCost() float64
GetName() string
}
When generics was introduced, a new way of writing interfaces was also done.
Can 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
}
Go doesn't have enums. It can be approximated, but it's not as robust as proper enums
We want to limit the checkPermission function to limit options
It should only take Read, Wrtie and Exec
We try to feed it two incorrect options. Admin and User
Trying to pass checkPermission Admin will fail as it's not of type perm
As the underlying type of perm is just a string, the var User = perm("user") doesn't cause a compiler error, even though it's not what we want to pass to the function
type perm string
const (
Read perm = "read"
Write perm = "write"
Exec perm = "execute"
)
var Admin = "admin"
var User = perm("user")
func checkPermission(p perm) {
// check the permission
}
checkPermission(Read) // will work
checkPermission(Admin) // won't compile
checkPermission(User) //will compile, but it shouldn't
checkPermission(perm(Admin) // will also compile when it shouldn't
// If your "enums" are numeric
const (
North = 0
East = 1
South = 2
West = 3
)
// Can use iota to enumerate automatically
const (
North = iota
East
South
West
)
// The best way to do it in Go is to define a type alias so different enums can't be compared to each other
type Direction int
const (
North Direction = iota
East
South
West
)
When decoding JSON, the struct and struct fields names must be public (capital first letter)
You can decode JSON bytes (or strings) into Go structs with json.Unmarshal or json.Decoder
Decode streams data from an io.Reader, while Unmarshal works with data that's already in []byte format.
Decoder can be more memory efficient as it doesn't load all the data into memory at once.
Unmarshal is ideal for small JSON data you already have in memory
Decoder is better when dealing with http responses since it works directly with io.Reader
[
{
"id": "001-a",
"title": "Unspaghettify code",
"estimate": 9001
}
]
As the JSON is an array, it can be decoded into a slice
type Issue struct {
Id string `json:"id"`
Title string `json:"title"`
Estimate int `json:"estimate"`
}
// res is a successful `http.Response`
var issues []Issue
decoder := json.NewDecoder(res.Body)
if err := decoder.Decode(&issues); err != nil {
fmt.Println("error decoding response body")
return
}
for _, issue := range issues {
fmt.Printf("Issue – id: %v, title: %v, estimate: %v\n", issue.Id, issue.Title, issue.Estimate)
// Issue – id: 001-a, title: Unspaghettify code, estimate: 9001
}
// res is an http.Response
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
var issues []Issue
if err := json.Unmarshal(data, &issues); err != nil {
return nil, err
}
type Board struct {
Id int `json:"id"`
Name string `json:"name"`
TeamId int `json:"team"`
TeamName string `json:"team_name"`
}
board := Board{
Id: 1,
Name: "API",
TeamId: 9001,
TeamName: "Backend",
}
data, err := json.Marshal(board)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
// {"id":1,"name":"API","team":9001,"team_name":"Backend"}
JSON keys are always strings, so if you're unsure of the JSON structure, you can can always unmarshall or decode into map[string]any
up to page 659