Skip to content

Instantly share code, notes, and snippets.

@tysonpaul89
Last active November 3, 2024 04:54
Show Gist options
  • Save tysonpaul89/da1c73e859381dfaf8f4187d3fafcf77 to your computer and use it in GitHub Desktop.
Save tysonpaul89/da1c73e859381dfaf8f4187d3fafcf77 to your computer and use it in GitHub Desktop.
Go Cheatsheet

Go

Go is a statically typed, compiled programming language designed at Google. Go is syntactically similar to C, but with memory safety, garbage collection, structural typing, and CSP-style concurrency.

This cheat sheet is prepared by refering the book Introducing Go Build Reliable, Scalable Programs by Caleb Doxsey

Topics

Single and Multi-line comments

// This is a single line comment in Go lang.

/*This is a multi-line comment in Go lang.
Contents within the comment blocks will be ignored.*/

Basic Data Types

Intergers

Type Range
uint8 0 to 255
uint16 0 to 65535
uint32 0 to 4294967295
uint64 0 to 18446744073709551615
int8 -128 to 127
int16 -32768 to 32767
int32 -2147483648 to 2147483647
int64 -9223372036854775808 to 9223372036854775807
byte Alias for uint8
rune Alias for int32
int Machine dependent type. Size of this type depends on the system architecture.
uint Machine dependent type. Size of this type depends on the system architecture.
uintptr Machine dependent type. Size of this type depends on the system architecture.

Floating-Points

Type Range
float32 IEEE-754 32-bit floating-point numbers
float64 IEEE-754 64-bit floating-point numbers

Variables

Variable name syntax

// Varibale Names must start with a letter and
// may contain letters, numbers, or the underscore symbol(_).
// Go uses camelCase variable names.
firstName := "Joe"
var lastName := "Jill"

Variable assignment

var x string = "Hello World!"

// No need to mention type. Go compiler will identify it based on the value.
x := "Hello World!"
var y = x

// Constants are varaibles whose value cannot be changed later.
const x string = "Hello World!"

String concatenation

x := "Hello "
x += "World!"
y := "Hello " + "World!"

Variable Scope

// A variable exists within the nearest curly braces ({}),
// or block, including any nested curly braces (blocks),
// but not outside of them.

package main
import "fmt"

var y int32 = 10

func main() {
 x := 5
 fmt.Println(y)
 fmt.Println(x)
 scopeExample()
}

func scopeExample() {
 fmt.Println(y)
 fmt.Println(x) // This will cause an error bcoz x doesn't exits in this function's scope.
}

Defining multiple varaibles

var (
 a = 1
 b = "Test"
)

fmt.Println(a) // 1
fmt.Println(b) // Test

Complex Data types

Arrays

An array is a numbered sequence of elements of a single type with a fixed length.

// Defining array
var x[5]int

// Array value assignment
x[0]=10
x[1]=20
x[4]=30
fmt.Println(x) // [10 20 0 0 30]

// Shorter syntax for creating arrays:
x := [5]float64{ 98, 93, 77, 82, 83 }
fmt.Println(x) // [98 93 77 82 83]

Slices

A slice is a segment of an array. Like arrays, slices are indexable and have a length. Unlike arrays, length of a slice is allowed to change.

x := make([]float64, 5) // This will make a slice of length 5
x[0] =  10
x[4] =  20
fmt.Println(x) // [10 0 0 0 20]

Slices are always associated with some array, and although they can never be longer than the array, they can be smaller.

x := make([]float64, 3, 5) // This will make of slice of length 3 from array of length 5

x := []int{1,2,3} // This is also valid

// Another way of creating slices is by using the [low : high] expression
x  := [5]int8{3, 6, 9, 12, 15}
y  := x[0:3]
fmt.Println(x) // [3 6 9]
// Here, low is the index of where to start the slice and high is the index of where to end it.

// Like python we can do this also
fmt.Println(x[:3]) // [3 6 9]
fmt.Println(x[2:5]) // [9 12 15]

Append to slice

append() function is used to add element to slice. While appeding if there’s not sufficient capacity in the underlying array, then a new array is created and all of the existing elements are copied over new one.

x := []int{1,2,3}
y : append(x, 4, 5)
fmt.Println(x) // [1 2 3]
fmt.Println(y) // [1 2 3 4 5]

Copy slice elements

x  := []int{1, 2, 3} // length = 3
y  :=  make([]int, 2) // length = 2
copy(y, x) // Copies elements in slice x to slice y
fmt.Println(x) // [1 2 3]
fmt.Println(y) // [1 2] Since length of y is 2, only 2 elements from x is copied.

Delete using index

package main

import "fmt"

func main() {
	numbers := []string{"one", "two", "three", "four", "five"}
	
 index := 2 // three
 
	// will delete three
	numbers = append(numbers[:index], numbers[index+1:]...)
	
 fmt.Println(numbers)
}

Output

[one two four five]

Maps

Maps is an unordered collection of key-value pairs. They are also called associative array, hash tables or dictionaries in other languages.

// Here keys are of type string and values will be int
// Also maps have to intialized before they can be used.
x := make(map[string]int)
x["one"] = 1
x["two"] = 2
fmt.Println(x) // map[one: 1 two:2]

To delete items from a map, uses the built-in delete function:

delete(x, "one")
fmt.Println(x) // map[two:2]

Accessing elements not defined in the map will return empty/zero value.

x  :=  map[string]int{ // Another way of creating maps
 "one": 1,
 "two": 2,
}

y  :=  map[string]string{
 "one": "Uno",
 "two": "dos",
}

fmt.Println(x["three"]) // 0
fmt.Println(y["three"]) // ""

// Better way to check map value
value, ok := x["three"]
if ok {
 fmt.Println(value)
}

// OR

if value, ok := x["three"]; ok {
 fmt.Println(value)
}

Complex map example:

product := map[int]map[string]string{
 101: map[string]string{
  "name":     "Iphone 12",
  "category": "Mobile",
 },
 102: map[string]string{
  "name":     "Mac Book Air",
  "category": "Laptop",
 },
}

fmt.Println(product[102]["name"]) // Mac Book Air

if element, ok := product[101]; ok {
 fmt.Println(element["name"]) // Iphone
}

Another example of complex Map

package main

import (
    "log"
)

func main() {
    //  Here interface{} means home map's key will be string but data can be of any type
    home := map[string]interface{}{
        "id": 123,
        "devices": []interface{}{
            map[string]interface{}{
                "device_id":    1,
                "device_label": "My PC",
            },
            map[string]interface{}{
                "device_id":    5,
                "device_label": "My iPhone",
            },
        },
        "name": "My Home",
    }

    home_id := home["id"].(int) // <-- Accessing interface data by converting it into correct type
    home_name := home["name"].(string)
    log.Println("Home Id: ", home_id)
    log.Println("Home Name: ", home_name)

    for _, device := range home["devices"].([]interface{}) {
        device := device.(map[string]interface{}) // <-- Converting device to correct type
        device_id := device["device_id"].(int)
        device_label := device["device_label"].(string)
        log.Println("Device Id:", device_id, " Device Label:", device_label)
    }

    // Another way to access device data
    d := home["devices"].([]interface{})
    d1 := d[0].(map[string]interface{})
    d_label := d1["device_label"].(string)
    log.Println(d_label)
}

Struct

Struct is a data type that contains named fields and methods.

package main

import "fmt"

type Animal struct { // <-- Struct definition
 name      string // <-- Stuct field
 legs, age int8
}

func (a *Animal) sound() string { // <-- Struct method definition
 if a.name == "Cat" {
  return "Meow"
 } else if a.name == "Tiger" {
  return "Roar"
 } else {
  return "bla bla bla"
 }
}

func main() {
 // Struct initialization method 1
 // Initializes struct with given name and legs value. For age a zero value will be assigned.
 cat := Animal{name: "Cat", legs: 4}
 cat.age = 2 // <-- Accessing struct fields

 // Struct initialization method 2
 tiger := Animal{"Tiger", 4, 5}

 // Struct initialization method 3
 // This will allocate memory for the fields, set zero value and returns a pointer of the struct.
 unknown := new(Animal)
 unknown.name = "unknown"

 describe(cat)
 // I am a Cat I have 4 legs, I am 2 years old and I make Meow sound.

 describe(tiger)
 // I am a Tiger I have 4 legs, I am 5 years old and I make Roar sound.

 describe(*unknown) // Since unknown is a pointer we have prefix *
 // I am a unknown I have 0 legs, I am 0 years old and I make bla bla bla sound.
}

func describe(animal Animal) {
 fmt.Println(
  "I am a", animal.name, "I have", animal.legs,
  "legs, I am", animal.age, "years old and",
  "I make", animal.sound(), "sound.",
 )
}

A struct’s fields usually represent the has-a relationship (e.g., a Circle has a radius ).

package main

import "fmt"

type Person struct {
 name string
}

func (p *Person) talk() {
 fmt.Println("Hi, my name is", p.name)
}

type Android struct {
 Person // <-- Person is a field of Android
 model  string
}

func main() {
 // Android{model: "2020", name: "Terminator"} will get an error.
 a := Android{model: "2020"}
 a.name = "Terminator" // <-- We can access Person.name directly
 a.talk()              // Hi, my name is Terminator

 b := new(Android)
 b.name = "Wall-E"
 b.model = "2021"
 b.talk() // Hi, my name is Wall-E
}

interface

Interfaces is a custom type that contains method or methods. you are not allowed to create an instance of the interface. But you are allowed to create a variable of an interface type and this variable can be assigned with a concrete type value that has the methods the interface requires.

package main

import (
 "fmt"
 "math"
)

type Geometry interface {
 area() float64
}

type Rectangle struct {
 width, height float64
}

type Circle struct {
 radius float64
}

func (r Rectangle) area() float64 { // <-- Implementing area() method
 return r.width * r.height
}

func (c Circle) area() float64 {
 return 2 * math.Pi * c.radius
}

func measure(g Geometry) { // <-- Interface type is used.
 fmt.Println(g)
 fmt.Println(g.area())
}

func main() {
 r := Rectangle{width: 3, height: 4}
 c := Circle{radius: 5}

 measure(r)
 // {3 4}
 // 12

 measure(c)
 // {5}
 // 31.41592653589793
}

Control Structures

for Loop

Go has only one loop, which can be used in differnt ways.

Normal looping

for i := 0; i <= 10; i++ {
 fmt.Println(i)
}
// Output:
// 0
// 1
// ...
// 10

As while loop

i := 0

for i <= 10 {
 fmt.Println(i)
 i++
}

// Output:
// 0
// 1
// ...
// 10

Looping an iterator

arr := [3]int8{10, 20, 30}

for  i, val  :=  range arr {
 fmt.Println(i, val)
}

// Output:
// 0 10
// 1 20
// 2 30

// If you don't want to the print loop index, then use underscore(_) to avoid unsed variable error.
for  _, val  :=  range arr {
 fmt.Println(val)
}

Functions

Normal function

package main

import "fmt"

func main() {
 c := total(2, 5)
 fmt.Println(c)
}

func total(a int, b int) int {
 return a + b
}

Functions with named return type

package main

import "fmt"

func main() {
 c := total(2, 5)
 fmt.Println(c)
}

func total(a int, b int) (sum int) {
 sum = a + b
 return
}

Multiple return values

package main

import "fmt"

func main() {
 res, isEvent := total(2, 5)
 fmt.Println(res) // 7
 fmt.Println(isEvent) // false
}

func total(a int, b int) (int, bool) {
 sum := a + b
 isEven := sum%2 == 0
 return sum, isEven
}

Variadic Functions

Function that can accept any number of parameters. Its similar to the *args in the python

package main

import "fmt"

func main() {
 res := total(2, 5, 3)
 fmt.Println(res) // 10
}

func total(args ...int) int {
 sum := 0
 for _, num := range args {
  sum += num
 }

 return sum
}

Closure

A function inside a function with access to a non-local variable is known as a closeure

package main

import "fmt"

func main() {
 x := 0
 increment := func() int { // This is a Closure
  x++ // This is non-local variable
  return x
 }

 fmt.Println(increment()) // 1
 fmt.Println(increment()) // 2
}

Recursion

Function that call itself during execution is called recursion

package main

import "fmt"

func main() {
 fmt.Println(factorial(3)) // 6
}

func factorial(x uint) uint {
 if x == 0 {
  return 1
 }
 return x * factorial(x-1)
}

init function

This function is present in every package and this function is called when the package is initialized. The main purpose of the init() function is to initialize the global variables that cannot be initialized in the global context. init() function is executed before the main(). Multiple init() function can be created in the same program and they execute in the order they are created.

Defer statement

Function with defer statement schedules a calls to run after the function completes. Its is usually used for performing clean up actions.

package main

import "fmt"

func main() {
 defer first()
 second()
 // Output:
 // Second
 // First
}

func first() {
 fmt.Println("First")
}

func second() {
 fmt.Println("Second")
}

panic and recover

panic and recover is simlar to try..catch in other languages. panic is used to throw and error and recover is used catch the error. In Go the recover function should be called inside a defer statement. Also the recover function should be called inside the function were is panic is callled.

package main

import "fmt"

func main() {
 div1 := division(1, 2)
 fmt.Println(div1) // 0.5

 div2 := division(3, 0) // Denominator Can't be zero.
 fmt.Println(div2) // 0
}

func division(numerator float64, denominator float64) float64 {
 defer handleError()

 if denominator == 0 {
  panic("Denominator Can't be zero.")
 }

 return numerator / denominator
}

func handleError() {
 err := recover()
 if err != nil {
  fmt.Println(err)
 }
}

Pointers

A pointer holds the memory address of a value.

package main

import "fmt"

func main() {
 x := 10
 fmt.Println("x =", x) // x = 10
 resetValue(&x) // & is must be used when passing pointer.
 fmt.Println("After reset x =", x) // After reset x = 0
}

func resetValue(val *int) { // The type of the pointer varaibile mmust have a * prefix.
 fmt.Println("val =", val) // val = 0xc000126010
 // To asign value to a pointer * must be prefixed with the variable name.
 fmt.Println("*val =", *val) // *val = 10
 *val = 0
}

We can also get a pointer using a built-in new() function.

package main

import "fmt"

func main() {
 x := new(int) // Gets the pointer
 fmt.Println("x =", x)   // x = 0xc0000b8010
 fmt.Println("*x =", *x) // *x = 0
 setValue(x)
 fmt.Println("*x =", *x) // *x = 20
}

func setValue(z *int) {
 *z = 20
}

Packages

Do the following to create and run a package.

  • If my_example is the project root then, create a directory named util
  • Create util.go inside the util directory and add the following code in it.
package util

func IsEven(num int) bool {
 if num%2 == 0 {
  return true
 } else {
  return false
 }
}
  • Go back to my_example directory, create main.go and add the following code.
package main

import (
 "fmt"
 "my_examples/util"
)

func main() {
 res := util.IsEven(4)
 fmt.Println(res) // true
}
  • Now run go run main.go

  • In a package, only the function name with upper camel casing is accessible from outside. Example: Average()

  • Function name with lower camel casing are private functions. Example: total()

Common Package Examples

strings operations

strings package is used to perform operations on strings.

package main

import (
    "log"
    "strings"
)

func main() {
    // Change to lower case.
    log.Println(strings.ToLower("Hello World!")) // hello world!

    // Change to upper case.
    log.Println(strings.ToUpper("Hello World!")) // HELLO WORLD!

    // String check if given sub-string present in a string.
    log.Println(strings.Contains("Hello World!", "World")) // true

    // To join list of strings into a single string.
    numbers := []string{"one", "two", "three"}
    log.Println(strings.Join(numbers, ", ")) // One, Two, Three

    // To split strings into a list of strings.
    numString := "uno, dos, tres"
    log.Println(strings.Split(numString, ", ")) // [uno dos tres]

    // String replace
    str := "one twenty one"
    log.Println(strings.Replace(str, "one", "three", 1))  // three twenty one
    log.Println(strings.Replace(str, "one", "three", 2))  // three twenty three
    log.Println(strings.Replace(str, "one", "three", -1)) // three twenty three
    log.Println(strings.ReplaceAll(str, "one", "three"))  // three twenty three

    // Trim trailing spaces
    testStr := strings.TrimSpace(" Test ")
    log.Println(testStr)
}

File write

os package is used for file write.

package main

import (
 "os"
)

func main() {
 f, err := os.Create("test.txt")
 if err != nil {
  return
 }

 defer f.Close()

 f.WriteString("Testing 124")
}

File read

io/ioutil package is use for file read.

package main

import (
    "log"
    "os"
)

func main() {
    f, err := os.ReadFile("test.txt")
    if err != nil {
        return
    }

    log.Println(f) // [72 101 108 108 111 32 87 111 114 108 100 33]
    str := string(f)
    log.Println(str) // Hello World!
}

Random number

package main

import (
    "log"
    "math/rand"
)

func main() {
    n := rand.Intn(1000)
    log.Println(n)
}

Goroutines

A goroutine is a function that is capable of running concurrently with other functions.

In order to run a function as a goroutine we have to use the go keyword. In the below example, if we run the code without go keyword, then we will get the following synchronous output.

2 is an even number.
4 is an even number.
6 is an even number.
Even numbers printed.
1 is an odd number.
3 is an odd number.
5 is an odd number.
Odd numbers printed.

When we run the below example code as goroutines, we will get the following asynchronous output. This proves that the functions were running concurrently.

Even numbers printed.
Odd numbers printed.
2 is an even number.
1 is an odd number.
4 is an even number.
3 is an odd number.
5 is an odd number.
6 is an even number.
package main

import (
 "log"
 "time"
)

func main() {
 arr := []int{1, 2, 3, 4, 5, 6}

 go printEvenNumbers(arr) // <-- Calling function as a goroutine
 log.Println("Even numbers printed.")

 go printOddNumbers(arr) // <-- Calling function as a goroutine
 log.Println("Odd numbers printed.")

 var someInput string
 log.Scan(&someInput) // Scan is added to prevent script from exiting.
}

func printEvenNumbers(nums []int) {
 for _, num := range nums {
  if num%2 == 0 {
   log.Println(num, "is an even number.")
   time.Sleep(250 * time.Millisecond) // Sleeps for 0.25 sec
  }
 }
}

func printOddNumbers(nums []int) {
 for _, num := range nums {
  if num%2 != 0 {
   log.Println(num, "is an odd number.")
   time.Sleep(250 * time.Millisecond) // Sleeps for 0.25 sec
  }
 }
}

Channels

Channels are concurrency mechanism that allows goroutines to communicates and synchronize with each other.

  • Channels are strongly typed.
  • Un-buffered by default, ie sender must wait for a receiver to be ready before sending a value.
  • Channels can be buffered, allowing a certain number of values to be stored temporarily before receiver is available.
  • Channels can be closed to signal that no more values will be sent. Receivers can check if a channel is closed using the ok value returned by the recover operation.

Un-buffered Channel Example

package main

import (
    "fmt"
)

func main() {
    // Un-buffered channel
    ch1 := make(chan string)
    go func() {
        ch1 <- "Hello" // <--- Sending value to channel
        close(ch1) // <--- Closing the channel
    }()

    fmt.Println(<-ch1) // <--- Receiving the value from channel
}

Output

Hello

Buffered Channel Example

package main

import (
    "fmt"
)

func main() {
    // Buffered channel that can store 2 values
    ch1 := make(chan string, 2)
    go func() {
        ch1 <- "Hello" // <--- Sending value to channel
        ch1 <- "World" // <--- Sending value to channel
        close(ch1)     // <--- Closing the channel
    }()

    fmt.Println(<-ch1) // <--- Receiving first value
    fmt.Println(<-ch1) // <--- Receiving second value
}

Output

Hello
World

Channel Directions Example

package main

import (
    "fmt"
)

func main() {
    ch := make(chan string, 2)

    handler(ch)

    sender(ch)

    receiver(ch)

    close(ch) // Build-in function to close the channel
}

func sender(ch chan<- string) { // <-- This means channel can only send data
    ch <- "Tim"
}

func receiver(ch <-chan string) { // <-- This means channel can only receive data
    msg := <-ch
    fmt.Printf("Hello %s", msg)
}

func handler(ch chan string) { // <-- This argument means ch can send and receive data
    ch <- "Joe"
    received_msg := <-ch

    out := fmt.Sprintf("Hello %s", received_msg)
    fmt.Println(out)
}

Output

Hello Joe
Hello Tim

Channel Synchronization Example

package main

import (
    "fmt"
    "log"
)

func main() {
    var result chan float64 = make(chan float64)
    // result := make(chan float64) <-- this is also valid

    arr1 := []float64{1, 2, 3, 4, 5, 6}
    arr2 := []float64{10, 20, 30, 40, 50, 60}
    go findAverage(float64(len(arr2)), result)
    go findTotal(arr1, result)
    go findAverage(float64(len(arr1)), result)
    go printResult(result)
    go findTotal(arr2, result)
    go printResult(result)

    var someInput string
    fmt.Scan(&someInput) // Scan is added to prevent script from exiting.
}

func findTotal(nums []float64, sum chan<- float64) { // <-- Here sum can only used for send message to channel.
    total := 0.0
    for _, num := range nums {
        total += num
    }
    log.Println("Sum =", total)
    sum <- total // <-- Sending message to chanel
}

func findAverage(length float64, result chan float64) { // <-- Here result is bidirectional. ie, it can send and receive message to/from channel.
    total := <-result // <-- Receiving message from chanel
    avg := total / length
    log.Println("Average calculated for", total, "/", length)
    result <- avg
}

func printResult(avg <-chan float64) { // <-- Here avg can only used for receiving message from channel.
    log.Println("Average =", <-avg)
}

Output of the above example:

Sum = 21
Average calculated for 21 / 6
Average = 3.5
Sum = 210
Average calculated for 210 / 6
Average = 35
  • In the above code, we called the findAverage(float64(len(arr2)), result) first but this function was executed after findTotal(arr2, result) because it was waiting for the message from channel.
  • Here code executoin was synchronous because the execution order was findTotal, findAverage and printResult.
  • We can use the channel direction on the channel type to restrict channel operations. In the above code, we use channel direction in findTotal function to allow only sending messages to channel and in the printResult function to allow only receiving messages from channel.
  • The findAverage is bidirectional because it can send and receive message from channel.

Select Example

select lets you wait on multiple channel operations.

package main

import (
    "fmt"
    "time"
)

func main() {
    // For our example we'll select across two channels.
    c1 := make(chan string)
    c2 := make(chan string)

    // Each channel will receive a value after some amount
    // of time, to simulate e.g. blocking RPC operations
    // executing in concurrent goroutines.
    go func() {
        time.Sleep(1 * time.Second)
        c1 <- "one"
    }()

    go func() {
        time.Sleep(2 * time.Second)
        c2 <- "two"
    }()

    // We'll use `select` to await both of these values
    // simultaneously, printing each one as it arrives.
    // Loop is needed because we have 2 channels, otherwise select will stop execution when it receives first value
    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-c1:
            fmt.Println("received", msg1)
        case msg2 := <-c2:
            fmt.Println("received", msg2)
        }
    }
}

Output

received one
received two

Select with Timeout Example

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "one"
    }()

    select {
    case msg := <-ch1:
        fmt.Println("ch1 received", msg)
    case <-time.After(3 * time.Second): // <--- Timeout logic will execute after 3 seconds
        fmt.Println("ch1 Timeout after 3 seconds")
    }

    ch2 := make(chan string, 2)

    go func() {
        time.Sleep(5 * time.Second)
        ch2 <- "three"
    }()

    select {
    case msg := <-ch2:
        fmt.Println("ch2 received", msg)
    case <-time.After(2 * time.Second): // <--- Timeout logic will execute after 2 seconds
        fmt.Println("ch2 Timeout after 3 seconds")
    }
}

Output

ch1 received one
ch2 Timeout after 3 seconds

In ch1 anonymous function slept only for 1 second which is less than the 3 second timeout interval so it received the data.

In ch2 timeout was only 2 seconds and the anonymous function was sleeping for 5 seconds, so it timeout executed first

Object-Oriented Programming

Go does not strictly support object orientation but is a lightweight object Oriented language.

Go does not have classes instead it uses struct. Struct are user defined types that hold just the state and not the behavior. struct can be used to represent a complex object and can add functions to the struct that can add behavior to it.

package main

type Book struct {
    name   string
    author string
    page   int
}

func (book Book) print_details() {
 fmt.Printf("Book Name %s", book.name)
}

func main() {
    hp := Book{name: "Harry Potter", author: "J K Rowling", page: 200}
    hp.print_details()
}

Inheritance

Since Golang does not support classes, inheritance takes place through struct embedding.

We cannot directly extend structs but rather use a concept called composition where base structs can be embedded into a child struct and the properties and methods of the base struct can be directly called on the child struct.

// main.go
package main

import "fmt"

// Base/Parent Struct
type Item struct {
 price float64
 qty   int
}

func (item Item) total() float64 {
 qty := float64(item.qty)
 return qty * item.price

}

// Child Struct
type Tax struct {
 rate float64
 Item
}

func (tax Tax) post_tax_total() float64 {
 total := tax.total()
 return total + (total * tax.rate)
}

func main() {
 bill := Tax{
  rate: 0.2,
  Item: Item{price: 100.0, qty: 10},
 }
 fmt.Println("Price of the item:", bill.price)
 fmt.Println("Quantity:", bill.qty)
 fmt.Println("Total before tax:", bill.total())
 fmt.Printf("Tax: %v%%\n", bill.rate*100)
 fmt.Println("Total after tax:", bill.post_tax_total())
}

Output

Price of the item: 100
Quantity: 10
Total before tax: 1000
Tax: 20%
Total after tax: 1200

Enum

Go does not have enums but following code show one of the ways to emulate the behavior.

package main

import (
    "log"
)

func main() {
    type PhoneCodeType struct{ IT, US, IN int }

    var PhoneCode = PhoneCodeType{
        IT: 39,
        US: 1,
        IN: 91,
    }

    log.Println("Phone Country Code of Italy is", PhoneCode.IT)
}

Handling JSON

Strut to JSON string

package main

import (
    "encoding/json"
    "fmt"
)

// In the below struct the `json:"<<name>>"` is called the JSON tag.
// json.Marshal() uses this JSON struct name in final json string
type Mark struct {
    Subject string `json:"subject"`
    Score   int    `json:"score"`
}

type Student struct {
    Name  string `json:"student_name"` // <--- Notice the Struct property name and JSON tag name is different
    Marks []Mark `json:"marks"`
}

func main() {
    marks := []Mark{
        {Subject: "Maths", Score: 60},
        {Subject: "English", Score: 70},
    }
    student := Student{
        Name:  "Jack",
        Marks: marks,
    }

    str_data, err := json.Marshal(student)
    if err != nil {
        fmt.Println(err.Error())
    }

    fmt.Println(string(str_data))
}

Output

{"student_name":"Jack","marks":[{"subject":"Maths","score":60},{"subject":"English","score":70}]}

JSON string to Struct

package main

import (
    "encoding/json"
    "fmt"
)

type Mark struct {
    Subject string `json:"subject"`
    Score   int    `json:"score"`
}

type Student struct {
    Name  string `json:"student_name"`
    Marks []Mark `json:"marks"`
}

func main() {
    json_string := `{"student_name":"Jill","marks":[{"subject":"Maths","score":50},{"subject":"English","score":75}]}`

    var student Student
    err := json.Unmarshal([]byte(json_string), &student)
    if err != nil {
        fmt.Println(err.Error())
    }

    fmt.Println(student.Name)
    for _, mark := range student.Marks {
        fmt.Printf("Subject: %s, Score: %d\n", mark.Subject, mark.Score)
    }
}

Output

Jill
Subject: Maths, Score: 50
Subject: English, Score: 75

Parsing complex JSON string

package main

import (
    "encoding/json"
    "fmt"
)

type Window struct {
    Title string `json:"title"`
}

type Image struct {
    Src  string `json:"src"`
    Name string `json:"name"`
}

type Widget struct {
    Image Image       `json:"image"`
    Text  interface{} `json:"text"`
}

type WebResponse struct {
    Widget Widget `json:"widget"`
}

const json_data string = `
    {
        "widget": {
            "debug": "on",
            "window": {
                "title": "Sample Konfabulator Widget",
                "name": "main_window",
                "width": 500,
                "height": 500
            },
            "image": {
                "src": "Images/Sun.png",
                "name": "sun1",
                "hOffset": 250,
                "vOffset": 250,
                "alignment": "center"
            },
            "text": {
                "data": "Click Here",
                "size": 36,
                "style": "bold",
                "name": "text1",
                "hOffset": 250,
                "vOffset": 100,
                "alignment": "center",
                "onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;"
            }
        }
    }`

func main() {
    var response WebResponse
    if err := json.Unmarshal([]byte(json_data), &response); err != nil {
        fmt.Println(err.Error())
    }

    fmt.Printf("%+v\n", response)

    // You can only access properties that is defined corresponding struct types
    // In the above json widget.image object contains other properties like hOffset, vOffset and alignment
    // but we will not be able to access it since we only defined Src and Name in the Image struct
    fmt.Println(response.Widget.Image.Src)
    fmt.Println(response.Widget.Image.Name)

    // While parsing the json, properties that is not defined in the struct like widget.image, widget.debug, etc will not parsed
    // If you don't want to define every field in the response json, the you can define it as interface{}
    // Since we have defined  WebResponse.Widget.Text as interface{} we were able to parse its data
    text := response.Widget.Text.(map[string]interface{})
    fmt.Println(text["style"].(string))
    fmt.Println(text["size"].(float64))
}

Output

{Widget:{Image:{Src:Images/Sun.png Name:sun1} Text:map[alignment:center data:Click Here hOffset:250 name:text1 onMouseUp:sun1.opacity = (sun1.opacity / 100) * 90; size:36 style:bold vOffset:100]}}
Images/Sun.png
sun1
bold
36

Useful Utilities

Capitalize

Upper cases the first letter of the given string

package main

import (
    "fmt"
    "unicode"
)

func main() {
    fmt.Println(capitalize("jack"))
    fmt.Println(capitalize("Jill"))
}

// Upper cases the first letter of the given string
func capitalize(input string) string {
    if len(input) == 0 {
        return input
    }

    // Convert the first letter to upper case
    firstLetter := unicode.ToUpper(rune(input[0]))

    return string(firstLetter) + input[1:]
}

Output

Jack
Jill

Map Key By Value

Gets the value of the map by its key

package main

import (
    "fmt"
)

func main() {
    numMap := map[int]string{
        1: "One",
        2: "Two",
        3: "Three",
    }

    fmt.Println(GetMapKeyByValue(numMap, "Two"))

    esNumMap := map[string]string{
        "one":   "uno",
        "two":   "dos",
        "three": "tres",
    }
    fmt.Println(GetMapKeyByValue(esNumMap, "tres"))
}

// Gets the value of the map by its key
func GetMapKeyByValue[K, V comparable](inputMap map[K]V, searchValue V) K {
    for key, value := range inputMap {
        if searchValue == value {
            return key
        }
    }

    // Return default value of the type K when there are no results
    var zeroKey K
    return zeroKey
}

output

2
three

Json String to Map

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    json_str := `{"student_name":"Jill","marks":[{"subject":"Maths","score":50},{"subject":"English","score":75}]}`
    student := JsonToMap(json_str)
    fmt.Printf("%+v\n", student)
    fmt.Println(student["student_name"])
}

// JSON string to Map
func JsonToMap(jsonStr string) map[string]interface{} {
    result := make(map[string]interface{})
    json.Unmarshal([]byte(jsonStr), &result)
    return result
}

output

map[marks:[map[score:50 subject:Maths] map[score:75 subject:English]] student_name:Jill]
Jill

Beatify JSON string

To beatify a json string. Useful when you want to display the output in an HTML page

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
)

func main() {
    json_str := `{"student_name":"Jill","marks":[{"subject":"Maths","score":50},{"subject":"English","score":75}]}`
    fmt.Printf("%+v", JsonPrettyPrint([]byte(json_str)))
}

// Function to beatify a json string. Useful when you want to display the output in an HTML page
func JsonPrettyPrint(in []byte) string {
    var out bytes.Buffer
    err := json.Indent(&out, in, "", "&nbsp;&nbsp;")
    if err != nil {
        return string(in)
    }
    return out.String()
}

output

{
&nbsp;&nbsp;"student_name": "Jill",
&nbsp;&nbsp;"marks": [
&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"subject": "Maths",
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"score": 50
&nbsp;&nbsp;&nbsp;&nbsp;},
&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"subject": "English",
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"score": 75
&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;]
}

Go Coding Standards

Project Structure

A well-structured Go project is essential for maintaining code clarity, scalability, and testability, especially for REST API applications. Here's a recommended project structure:

your-project-name/
├── cmd/
│   └── your-api/
│       ├── main.go
│       └── server.go
├── internal/
│   ├── config/
│   │   ├── config.go
│   │   └── config.toml
│   ├── database/
│   │   ├── db.go
│   │   └── sql.go
│   ├── handler/
│   │   ├── healthcheck.go
│   │   └── user.go
│   ├── middleware/
│   │   ├── auth.go
│   │   └── logger.go
│   ├── model/
│   │   ├── user.go
│   │   └── error.go
│   └── repository/
│       ├── user.go
│       └── error.go
├── pkg/
│   ├── utils/
│   │   ├── http.go
│   │   └── errors.go
├── go.mod
├── go.sum

Breakdown of the Structure:

  • cmd Directory:
    • Contains the main executable files for your application.
    • main.go: Entry point of the application, sets up the server and starts listening.
    • server.go: Defines the HTTP server, routes, and middleware.
  • internal Directory:
    • Houses all the internal packages of your application.
    • config: Manages configuration files (e.g., config.toml) and provides functions to access configuration values.
    • database: Handles database interactions (e.g., SQL queries, ORM).
    • handler: Defines HTTP handlers for different API endpoints.
    • middleware: Implements middleware functions (e.g., authentication, logging, error handling).
    • model: Defines data structures representing your application's domain models.
    • repository: Defines interfaces and implementations for interacting with data storage (e.g., databases).
  • pkg Directory:
    • Contains reusable packages that can be used across multiple projects.
    • utils: Provides utility functions for common tasks (e.g., HTTP requests, error handling). Key Principles:
  • Clear Separation of Concerns: Each directory and package has a specific responsibility.
  • Dependency Injection: Use dependency injection to decouple components and make testing easier.
  • Modular Design: Break down your application into smaller, independent modules.
  • Error Handling: Implement robust error handling and logging mechanisms.
  • Testing: Write unit, integration, and end-to-end tests to ensure code quality.
  • Code Formatting: Use a linter like gofmt to enforce consistent code style.
  • Version Control: Use a version control system like Git to manage your codebase effectively.

A more detailed project structre documentation is available in this golang-standards/project-layout repo

File Naming Conventions

  • Use lowercase letters for filenames.
  • Separate words with underscores if necessary.
  • Keep file names short, descriptive, and relevant to the functionality.

Examples:

  • main.go
  • user_controller.go
  • http_server.go

Variable and Function Naming Conventions

  • CamelCase is used for naming variables, functions, and types.
  • PascalCase (start with uppercase) for exported variables, constants, types, and functions (visible outside the package).
  • camelCase (start with lowercase) for unexported variables, constants, types, and functions (not visible outside the package).

Examples:

  • Exported: type User struct, func GetUserName() string
  • Unexported: type user struct, func getUserName() string

Variable Naming:

  • Use meaningful names that reflect the purpose.
  • Use short names for local variables (like i, j for loops), but avoid excessive abbreviations.

Constants:

  • Use uppercase for constants.

Example:

const MaxConnections = 100
const defaultTimeout = 30

Code Formatting and Style

  • Follow standard Go formatting (gofmt). This tool automatically formats your code to follow Go standards, making it consistent and readable.
  • Indent code using tabs, not spaces.
  • Place opening braces on the same line as the statement.

Example:

func main() {
    if condition {
        fmt.Println("Hello, World!")
    }
}

Use Go tools for formatting:

  • gofmt
  • goimports (handles imports along with formatting)

Best Practices

Error Handling

  • Always check for errors and handle them appropriately.
  • Return errors as the last value of a function. Use the error type for better readability.

Example:

func readFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close()
    // Proceed with reading the file
    return nil
}

Avoid Global Variables

  • Use local variables or struct fields whenever possible to avoid potential conflicts and unexpected behaviors.

Use Short Variable Names for Small Scopes

  • For loop counters and small scopes, use short names like i, j, or c. For larger scopes, prefer descriptive names.

Prefer := for Short Declarations

  • Use := to declare and initialize variables within a function.

Example:

count := 10

Organize Imports

  • Separate standard library imports, third-party libraries, and local packages with blank lines.

Example:

import (
    "fmt"
    "os"
    
    "github.com/example/project"
    
    "myproject/utils"
)

Keep Functions Short and Focused

  • Functions should do one thing and do it well. Break down complex logic into smaller functions for better readability and maintainability.

Example:

func main() {
    setupServer()
    handleRequests()
}

Avoid Using else if not Necessary

  • Return early to avoid nested code blocks.

Example:

func validateUser(user *User) error {
    if user == nil {
        return errors.New("user cannot be nil")
    }
    // Proceed with validation
    return nil
}

Use defer for Resource Cleanup

  • Use defer to close resources like files and connections, ensuring they are always properly closed.

Example:

func processFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close()
    // Process file content
    return nil
}

Go Doc Comments

  • Use comments to explain the purpose of a function, struct, or package. Exported items should have comments starting with their names.

Example:

// AddNumbers adds two integers and returns the result.
func AddNumbers(a, b int) int {
    return a + b
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment