Learning goLang

dbillinghamuk
10 min readMay 22, 2024

--

Documenting the initial learning of the go programming language

Getting started

go needs to be installed. Uses the go command in terminal. Install vscode go extension.

Hello world

// file named main.go

package main
import "fmt"
func main() {
fmt.Println("hello world")
}

Run using: go run main.go

Commands

run -> compiles files and runs result
build -> just compiles, creates executable in root, run directly e.g. ./main
fmt -> formats code in files
install / get -> access to other packages
test -> runs test files

Two types of package

Executable -> Generates executable file (only main)
Reusable -> Helpers, libraries for reusable logic (any other name than main)

package main

What is package main?

Assigns all files, with the declaration at the top, to that package, it’s a way of grouping files.

package main is the only package that can be used to create executable
E.g. package me when built would not create executable

package main must always have a func called main

importing

import keyword is used to access code within another package (fmt is a default go package)

go package lib: https://pkg.go.dev/std

// single import
import "fmt"


// importing multiple packages
import (
"fmt"
"strings"
)

func main

func keyword has function name, args and body

File structure

Always; package > import -> func/variables etc

Variable declarations

E.g. var helloWorld sting = "HelloWorld"
Statically typed, (not dynamic like vanilla JS)
(Has type inference so type declaration here not needed var helloWorld = "HelloWorld")
Has all the basic types you’d expect
Has a shorthand helloWorld := "HelloWorld"
:=
can only be used for new variable declarations
Seems like variables are mutable
Note: Shorthand syntax seems to only work within a func where as full declaration will work outside
Variables can be declared and then assigned a value later

 var helloWorld string
helloWorld = "HelloWorld"

Functions

func getHelloWorld() string {
return "hello world"
}

//To call and assign it to a variable

func main() {
helloWorld := getHelloWorld()
}

funcs must specify a return type, can not be inferred
Variables can be passed into the func; func getHelloWorld(me string)

They can return more than one value:

func getHelloWorld() (string, string) {
return "hello world 1", "hello world 2"
}

//To call and assign it to a variable

func main() {
helloWorld1, helloWorld2 := getHelloWorld()
}

Lists / For loops

Two structures:

  • Array -> Fixed length
  • Slice -> Variable length

Both must contain the same types

// declares a new array/slice of string and assigned two values to it
myArr := []string {"a", "b"}

// adds value to the slice (NB. append does not mutate myArr)
myArr = append(myArr, "c")

// loops over and prints, range keyword used to iterate over slice
for index, item := range myArr {
fmt.Println(index, item)
}
// why := here? because on every iteration we need to redeclare

for {
// way to do an infinate loop
}

Note, if the index is not needed you can replace with underscore to avoid syntax error.
They are zero indexed; myArr[0] returns "a"
Built in to accept a range; myArr[0:2] returns "a", "b"
Values can be omitted to include all;
myArr[:2] returns "a", "b"
myArr[2:] returns "c"

Language type

Not an OOP or pure functional language. Very similar to javascript using typescript.

Using types

Declaring a type and using it

// list

type deck [] string // Note seems convention for type starts lowercase
myDeck := deck {"a", "b"}

// single type

type colour string
myColour := colour("red")
/*
Not initialising with values is different
between single types and list types
*/

Receiver function

func (t type) print() {}

Any variables of that type will get access to the function.

Example:

// deck.go

package main

import "fmt"

type deck [] string

func (d deck) print() {
for i, card := range d {
fmt.Println(i, card)
}
}

When initialising and calling the function

// main.go

myArr := deck {"a", "b"}
myArr.print()

Note how we’re calling myArr.print() The print func automatically becomes a property of the deck type.
Similar to this in javascript.
Convention is that the type referenced in the receiver is a single letter; (d deck).

Opinion; not sure why this extra complication is needed over just passing the variable as an argument in the function.

Type conversion

Convert one type into another type

[]byte("HelloWorld") // converts string to byte slice

Strings util package

// useful functions such as join

import "strings"

strings.Join([]string(deck), ", ")
// converts deck type -> string array -> string

OS util package

// writting a file to disk

os.WriteFile("myFile", []byte("my string"), 0666)

// reading a file

bs, err := os.ReadFile("myFile")
if err != nil { // common practise for checking for errors
fmt.Println("Error:", err)
os.Exit(1) // will exit the promgram with an error
}
string(bs) // type converison byte slice to string

Swap func

Way to swap values in a slice

func swap () []string {
v := []string{"1","2","3","4"}

println("v 1: ", toString(v))

v[0], v[3] = v[3], v[0]

println("v 2: ", toString(v))

return v
}

// output
// v 1: 1, 2, 3, 4
// v 2: 4, 2, 3, 2

Testing

To get setup with testing we first need to run: go mod init {name} to initialise a go.mod file. Then we can run go test

Dont use third parties such as mocha, jest, selenium etc

Tests go in files {name}_test.go

Testing our swap func

// main_test.go

func TestSwap(t *testing.T) { // Convention for test naming pascal case
v := swap()
if len(v) != 4 {
t.Errorf("incorrect swap length")
}
if v[0] != "4" {
t.Errorf("incorrect first value")
}
if v[3] != "1" {
t.Errorf("incorrect last value: %v", v[3]) // Errorf - formatted string
}
}

To test a function that saves to disk, we don’t mock the behaviour, we handle the clearing of the file at the start and the end of the test.

Struct data structure

Collection of properties that are related, (like an object in javascript).

// Ceating a struct type

type person struct { // Lower case types seems to be convention
firstName string
lastName string
}

// Initialising
joe := person{"joe", "bloggs"} // Not recommended, relies on order
// or
joe2 := person{
firstName: "joe",
lastName: "bloggs",
}

// Mutating
joe.firstName = "jane"

Declaring a type with no values e.g. var joe person , go will assign whats called zero values to the properties, (not undefined):
- int/float -> 0
- string -> “”
- boolean -> false

To print out the struct in a more readable way use fmt.printf("%+v", joe)

Structs can be embedded in structs, they can have properties that are other custom structs, not just core types like string etc..

type contactInfo struct {
email string
}

type person struct {
...,
contact: contactInfo
// or just contactInfo, if we're happy with the property being called that e.g
contactInfo
}

joe := person{
firstName: "joe",
contact: contactInfo{
email: "joe@bloggs.com",
},
}

Receiver functions work on structs the same way as when using them on types which are base types.

Pass by value

golang is referred to as a pass by value language, when a variable is passed to a function or a receiver function is called on a variable, golang creates a copy and passes that, so any mutations that happen in that function happen on that copy and not the actual value passed in.

func (p person) updateName(name string) {
p.firstName = name
}

p := person{}

p.updateName("joe")

fmt.println(p) // -> empty struct | update in fn will get updated to a copy

Pointers

To allow updating of values we use pointers. We get the pointer, (address where the value is pointing to in memory), and pass that to update function.

func (pPointer *person) updateName(name string) { // * is a type description, type pointer to a deck
(*pPointer).firstName = name // * operator gets the value for that pointer
}

p := person{}

pPointer := &p // & operator gets the pointer for that value

pPointer.updateName("joe")

fmt.println(p) // -> todo

Pointer operators:
- & -> gets the pointer for a value
- * -> gets the value for a pointer

Can be confusing that * means two different things; when used on a type it is to say the type is a pointer to that type, when used on a value it’s a pointer to that value.

Pointers would be needed for; int, float, string and struct types.

Pointer shorthand

Because the above updateName function has a receiver of type *person golang allows for a person type OR a pointer person type to be passed.

// longhand

pPointer := &p
pPointer.updateName("joe")

// shorthand

p.updateName("joe")

Reference vs Value types

When a slice is passed to a function it will mutate the passed in value, (no need to get a pointer to the slice). This is because when a slice is created it creates the slice and an array behind the scenes. This array has its own address in memory. So when the slice is passed to the function it is copied as you would expect, but the copy contains a pointer to the same underlying array as the original.

Map

Collection of key value pairs, (like object in javascript). Keys and values must all be the exact same type.

colours := map[string]string{ // keys and values both type string
"red": "#ff0800",
"green": "#00ff08",
}

// to create without values
var colours map[string]string
// or
colours := make(map[string]string) // make built in golang function

// adding
colours["white"] = "#fff"

// removing
delete(colours, "white") // delete built in golang function

// iterate
for key, value := range colour {
...
}

Maps vrs structs

Structs would seem to be used over maps in most cases imo.

Interfaces

Allow functions with common code to be called on similar types.

type bot interface {
getGreeting() sting
}

type eBot struct{}
type sBot struct{}

func (e eBot) getGreeting() string{...}
func (s sBot) getGreeting() string{...}

func print (b bot){...}

eb := eBot{}
sb := sBot{}

print(eBot)
print(sBot)

// Becuase both bots call getGreeting and return a string they are classified
// as being a member of the bot interface and the same print function can
// be called on both eBot and sBot types

There are two types of type; concrete (string, struct, eBot etc..) and interface (bot). You can not create a value out of a interface type like you can a concrete type.
Interfaces are implicit, (you dont have to say eBot implements the bot interface, its inferred).
Interfaces can be nested, so one interface can have properties which are other interfaces.

Making a http request

import (
"fmt"
"net/http"
"os"
)

resp, err := http.Get("https://google.com")

if err != nil {
fmt.Println("Error", err)
os.Exit(1)
}

fmt.Println(resp) // outputs the raw response

// to outpout body

bs := make([]byte, 99999) // initialise byte slice for body to go into

bc, err := resp.Body.Read(bs) // read the body and add to byte slice

fmt.Println(string(bs))
fmt.Println("byte count", bc)
fmt.Println("err", err)

// or

io.Copy(os.Stdout, resp.Body) // documented below

Reader and Writer (with io.Copy example)

func Copy(dst Writer, src Reader) (written int64, err error)

This function will take a type that implements the Writer interface and a type that implements the Reader interface. It then copies the one to the other.

// a custom log writer to the console

type logWriter struct{}

func (logWriter) Write(b []byte) (int, error) {
fmt.Println(string(b))
fmt.Println("Using my custom writer")
return len(b), nil // int output is byte length
}

lw := logWriter{}

// as logWriter type implements a `Write` function it can be passed to Copy

io.Copy(lw, resp.Body)

Channels and Routines

Running requests in parallel rather than serial, (like Promise.all()in javascript).

A running process is one golang routine. Additional go routines can be created when dealing with concurrent requests which you want to hand off from the main thread.

Channels, are typed (must be the same type), and allow management of these routines, act like a two way messaging device.

Write to channel — c <- "value"
Read from channel (into variable) — i <- c

links := []string{}

c := make(chan string) // create channel of string

for _, link := range links {
go linkCheck(link, c)
// placing the keyword `go` infront of a func creates new go routine.
// this means that the http request will not wait for a response before
// continuing to the next check
}

func linkCheck(link string, c chan string) {
_, err := http.Get(link)
c <- "resposne" // add a value to the channel
...
}

// when run without a channel the program will just exit as the main
// thread will finish before the passed off routines have come back

// with channel

for i := 0, i <= len(links), i++ {
fmt.println(<-c)
// read from the channel n times until all respsonse recieved
// A VALUE COMING THROUGH c IS A BLOCKING CALL
}

By default, golang will use one cpu core and will use whats called the go Scheduler to orchestrate the routines, (still technically running one routines at a time). When multi cores are available the scheduler will assign routines across the cores and will be running multiple routines at the same time.

Concurrency is not parallelism

Concurrency — behaviour demonstrated given one core
Parallelism — behaviour demonstrated given multi core

Function literal

Same as anonymous function in javascript.

for l := range c {
go func(link) { // func within body of code defines function literal
time.sleep(5 * time.Second)
linkCheck(link, c)
}(l) // parentheses at the end executes the function literal
// we need to pass the l value so its scoped to the function literal
}

References

https://www.udemy.com/course/go-the-complete-developers-guide

Sign up to discover human stories that deepen your understanding of the world.

--

--

dbillinghamuk
dbillinghamuk

Written by dbillinghamuk

Software dev — Javascript, node, express, mongo, react, redux, rxjs, es6, ramda

No responses yet

Write a response