# Golang fundamentals

December 19, 2025 Golang Fundamentals

Go is a relatively easy language to pick up. This article walks through the essentials of the language.

Hello world

package main

import (
  "fmt"
)

func main() {
  fmt.Println("Hello World")
}

Importing Libraries

A single library can be imported as it has been in the hello world program above. If we need to import multiple libraries we should use the Go convention

/* not the Go convention */
import "fmt"
import "time"

/* Go convention */
import (
  "fmt"
  "time"
)

Note: If we import a library and don’t use it in our code, the GoLang compiler will raise an error preventing compilation.

Variables

Note: The memory size of any variable can be found using the following method.

import (
	"fmt"
	"unsafe"
)

func main() {
	var num int32 = 10
	fmt.Printf("bytes: %d\n", unsafe.Sizeof(num)) // size in bytes
}

Note: Remember that int32 is 2^4 bits i.e. 4 bytes in size.

Packages

You may have noticed the first line in our Hello World program: Every program must have at least one main package. We cannot have more than one package in a single directory.

main.go
pkg/
  user/
    user.go
// user.go
package user

import (
  "encoding/json"
)

type User struct {
  Id       uint   `json:"id"`
  Name     string `json:"name"`
  Email    string `json:"email"`
  Password string `json:"-"`
}

func (user User) Serialize() (string, error) {
  encoded, err := json.Marshal(user)
  if err != nil {
    return "", err
  }

  return string(encoded), nil
}

// implement the Stringer interface
func (u User) String() string {
  return fmt.Sprintf("User(id=%d, name=%q, email=%q)", u.Id, u.Name, u.Email)
}

func New(name, email string) User {
  return User{
    Id:       300,
    Name:     name,
    Email:    email,
    Password: "password",
  }
}
// main.go
package main

import (
  "fmt"
  "os"
  "sandbox/pkg/user"
)

func main() {
  user := user.New("Sample", "sample@site.com")
  encoded, err := user.Serialize()
  if err != nil {
    fmt.Fprintf(os.Stderr, "error: %v\n", err)
    os.Exit(1)
  }

  fmt.Println(encoded)
}
Double quotes vs. Single Quotes

In Go, double quotes are used to define strings and import modules. This is strictly enforced so using single-quotes in these cases will result in compilation errors.

The names of Imported Libraries can also be Aliased

// import strings as s
import s "strings"
Comments
// single line comment

/**
 *  Comment block (this style is not conventional in Go)
 *
 */
Logical operators
fmt.Println(true && false)	/* false */
fmt.Println(true || false)	/* true  */
fmt.Println(!true)		    /* false */
Data types
OperatorDescription
int, int8, int16, int32, int64Integers
uint, uint8, uint16, uint32, uint64Unsigned integers
float, float32, float64Floating point numbers
boolBoolean value
stringStream of characters
runeRune is used to represent Characters. It is an alias for int32
byteByte is an alias for uint8
Defining Variables
// defining and initializing without specifying the data type
a := 300
var b = 30.95

// specifying the data type
var c string = "This is a string"

// defining but not initializing
var d bool

// defining multiple variables at the same time
var e, f int = 32, 64
Multi-line Strings
fmt.Println(`
  This is a simple
  multiline message
  -- Good Bye`)
Constants

Constants are defined the same way as variables. We use the keyword const instead of var. Constants are immutable.

const name string = "Connor Kenway"
Find the type of a variable
var name string = "Kalahari"

func typeof(i interface{}) {
  fmt.Printf("%T\n", i)
}

// string
fmt.Printf("My name is %v and I am %v years old.", "User", 27)
fmt.Printf("It is %v that I am a programmer", true)
Converting Numbers to Strings
import (
  "fmt"
  "strconv"
)

func main() {
  num := 30
  fmt.Printf("%v, %T\n", num, strconv.Itoa(num))
}

// 30, string
Type Casting

Go doesn’t perform any implicit type conversions. All conversions must be explicit. Variables can be casted to any compatible data type, primitive or custom.

We can use the name of the data type as a function call for casting data types.

var num uint32 = 400
num2, ok := int64(num)
typeof(num2)

// int64
Type switch
func main() {
  var input any = false
  var message string

  switch t := input.(type) {
  case int:
    message = "int / bool was provided"

  case bool:
    message = "boolean was provided"

  case string:
    message = "string was provided: " + t // t is now string

  default:
    message = "unknown type provided"
  }

  fmt.Println(message)
}

Byte Slices

As mentioned earlier, Byte is simply an Alias for the Uint8 Primitive Data Type. Often functions return data in the form of Byte Slices. Here is what they look like

[76 111 114 101 109 32 105 112 115 117 109 32 100 111 108]
data, _ := os.ReadFile("./dir/sample")

/* method one */
fmt.Printf("%s\n", data)

/* method two */
fmt.Println(string(data))
String to Byte Slice
/* cast string as byte slice */
example := []byte("This is a simple Message")
fmt.Printf("%s\n", example)

String methods

package main

import (
  "fmt"
  "strings"
)

func main() {
  fmt.Println("Contains:  ", strings.Contains("test", "es"))
  fmt.Println("ToLower:   ", strings.ToLower("TEST"))
  fmt.Println("ToUpper:   ", strings.ToUpper("test"))
  fmt.Println("Count:     ", strings.Count("test", "t"))
  fmt.Println("HasPrefix: ", strings.HasPrefix("test", "te"))
  fmt.Println("HasSuffix: ", strings.HasSuffix("test", "st"))
  fmt.Println("Index:     ", strings.Index("test", "t"))
  fmt.Println("Split:     ", strings.Split("a-b-c-d-e", "-"))
  fmt.Println("Join:      ", strings.Join([]string{"a", "b"}, "-"))
  fmt.Println("Repeat:    ", strings.Repeat("a", 5))
  fmt.Println("Replace:   ", strings.Replace("foo", "o", "0", -1))
  fmt.Println("Replace:   ", strings.Replace("foo", "o", "0", 1))
  fmt.Println("TrimSpace: ", strings.TrimSpace("   test    "))
}

/*
Contains:   true
ToLower:    test
ToUpper:    TEST
Count:      2
HasPrefix:  true
HasSuffix:  true
Index:      0
Split:      [a b c d e]
Join:       a-b
Repeat:     aaaaa
Replace:    f00
Replace:    f0o
TrimSpace:  test
*/
String Formatting
PlaceholderDescription
%vInfer and Print any Data type
%+vPrint Struct with field names
%#vStructs with field value and struct name / declaration location
%dDecimal Number i.e. Base-10 representation
%bBinary Number i.e. Base-2 Representation
%xHexadecimal Number i.e. Base-16
%cCharacter representation of Number
%f, %gFloating Point Number
%sStrings
%pPointers
%6dSet Decimal Width to 6 characters, Right Aligned
%-5.2fSet Float width to 5 with 2 decimal places, Left Aligned
%qQuoted strings
%wErrors
%TType of variable

Loops

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

// while loop
i := 0
for i <= 10 {
  fmt.Println(i)
  i++
}

// infinite loop
for {
  fmt.Println("Infinite Loop")
}

// looping through arrays, slices, maps
names := []string{"Batman", "Ironman", "Superman"}
for _, name := range names {
  fmt.Println(name)
}

Break and Continue

The break and continue keywords work exactly the same way as C / C++

  • break ends the loop immediately
  • continue skips over the rest of the code in the loop and goes to the next iteration of the loop

Control-flow

marks := 90

if marks < 40 {
  fmt.Println("F: Fail")
} else if marks < 70 {
  fmt.Println("B: Average")
} else if marks <= 100 {
  fmt.Println("A: Excellent")
} else {
  fmt.Println("Invalid Marks")
}

// A: Excellent

Golang supports for-loop style preconditions in if statements as well. This can be helpful in making the code easier to understand.

if i := 30; i < 100 {
  fmt.Println("30 is less than 100")
}

Note: In the above example, the scope of i is limited to the block of If-statement.

Switch Statements
option := 4

switch option {
  case 1:
    fmt.Println("One")

  case 2, 3:
    fmt.Println("Two or Three")

  case 4:
    fmt.Println("Four")

  default:
    fmt.Println("Invalid Input")
}

// Four

One of the limitations of switch statements in C / C++ is that we cannot use conditions. In Go, any nested if statement can be written as a switch statement.

marks := 50

switch {
  case marks < 40:
    fmt.Println("F: Fail")

  case marks < 70:
    fmt.Println("B: Average")

  case marks <= 100:
    fmt.Println("A: Excellent")

  default:
    fmt.Println("Invalid Marks")
}

// B: Average

Arrays

/* declare an empty array */
var myArray [5]int
fmt.Println(myArray)

/* [0 0 0 0 0] */

/* access / change values from array */
myArray[0] = 300
myArray[4] = 220
fmt.Println(myArray)

/* [300 0 0 0 220] */

/* initialize array with values */
myArray2 := [5]int{ 23, 34, 29, 45, 56 }
fmt.Println(myArray2)

/* [23 34 29 45 56] */
arraySize := len(myArray)
/* declaring without values */
var myArr [3][3]int
fmt.Println(myArr)
/* [[0 0 0] [0 0 0] [0 0 0]] */

/* declaring with values */
myArray := [2][2]int {{ 30, 20 }, { 10, 40}}
fmt.Println(myArray)
/* [[30 20] [10 40]] */
Array length and capacity
nums := [10]int{1, 2, 3, 4, 5, 6}
fmt.Printf("len: %d, cap: %d\n", len(nums), cap(nums))

In the above example, both the length and capacity of the array will be 10. The first six elements have been given explicit values, the rest of the elements will be zero initialised.

Slices

Slices are like arrays but they are dynamic in size. Unlike an array we don’t specify the size / length of a slice when we initialise it.

/* declaration */
a := []int{}

/* declaration and initialization */
b := []int{10, 20, 30}

/* add elements */
my_slice = append(my_slice, 54, 32, 65)

Reason why they are called slices is because we can slice through them like Python slices

/* my_slice := [54 32 65 20 35 15 39] */

my_slice[1:3]		/* [32 65] */
my_slice[:4]		/* [54 32 65 20] */
my_slice[4:]		/* [35 15 39] */
Removing element from a slice
myArray := []int{1, 2, 3, 4, 5, 6, 7}

/* index value to be removed */
const remID int = 3

myArray = append(myArray[:remID], myArray[remID+1:]...)
fmt.Println(myArray)

In our example we slice up to the desired index (up to and not including) and unpack all the elements of the slice after the desired index. After the unpacking instructions will look like this behind the scenes

myArray = append(myArray[:remID], 5, 6, 7)

Note: are called the Spread Operator

Length vs. Capacity
slice := make([]int, 10 /* length */, 20 /* capacity (optional) */)

Length refers to the current number of elements in the slice. Initially, these elements will be zero-initialised (i.e. in case of int they will have the value of 0).

In the above example, the capacity is more than the length. This means that the first 10 elements will be zero-initialised and the remaining 10 elements will not be initialised.

All slices are backed by fixed-length arrays. length keeps track of current number of elements inserted into the slice. capacity is the actual full length of the backing array. If the length is same as capacity and we try to insert another element into the slice, the backing array will be doubled in size.

Example
func main() {
  s := []int{}
  fmt.Printf("len: %d, cap: %d\n", len(s), cap(s)) // 0, 0

  for i := 0; i < 100; i++ {
    s = append(s, 10)
    fmt.Printf("i: %d, len: %d, cap: %d\n", i, len(s), cap(s))
  }
}

In this example, the slice will start off with length and capacity of 0. capacity will then go like this as we keep inserting new elements inside the slice:

0 -> 1 -> 2 -> 4 -> 8 -> 16 -> 32 -> 64 -> 128 ...

Important: We can never index any un-initialised slice value, it will result in a panic.

Example
func main() {
  s := make([]int, 10, 20)

  for i := 0; i < 20; i++ {
    fmt.Println(s[i])
  }
}

In the above example, first 10 elements will be zero-initialised. The remaining (10) elements are not initialised. The above program will panic when trying to access s[10] because it has not been initialised.

Functions

func myFunction(a float64, b float64) float64 {
  return a + b
}

Before the curly braces we specify the return type of the function. This is only required if the function actually returns anything.

func myFunction(a, b float64) float64 {
  return a + b
}
func myFunction(a, b float64) (float64, float64) {
  return a+10, b+10
}
Variadic Functions

These are functions that take an arbitrary number of values as arguments.

func sum(nums ...int) int {
  result := 0

  for _, num := range nums {
    result += num
  }
  return result
}

func main() {
  fmt.Print(sum(1,2,3,4))
}

/* 10 */
myNumbers := []int{10, 20, 30, 40}

// passing a slice to variadic function
fmt.Print(sum(myNumbers...))
Higher order Functions
type wrappedFunc func (uint64) uint64

Maps

nationality := make(map[string]string)

// initialize with specified length
capitals := make(map[string]string, 10)

// add key values
nationality["Alice Eve"] = "British"
nationality["George Clooney"] = "American"
nationality["Trevor Noah"] = "South African"


// accessing a value using key
fmt.Println(nationality["Alice Eve"])

// count number of KV pairs in map
len(nationality)
nationality := map[string]string{
  "Alice Eve": "British",
  "Trevor Noah": "South African",
}
// Deleting values from Map
delete(nationality, "George Clooney")

Note: A map can always grow in memory, but it can never shrink. This can potentially cause memory leaks.

Another example
func main() {
	capitals := map[string]string{
		"Pakistan": "Islamabad",
		"China":    "Beijing",
		"Britain":  "London",
	}

    // access element from map, with error handling
	pak, ok := capitals["Pakistan"]
	if !ok {
		fmt.Fprintf(os.Stderr, "Not found\n")
		os.Exit(1)
	}

	fmt.Printf("found: %s\n", pak)
}

Sets

Go doesn’t have built-in set data-structure. However, we can use map as a set.

import (
	"fmt"
	"golang.org/x/exp/constraints"
)

// constraints.Ordered is an interface which allows all types of numbers
// and string types. Values in map are of type struct{} to prevent unnecessary
// memory usage.
type Set[T constraints.Ordered] map[T]struct{}

func (s Set[T]) Add(value T) {
	s[value] = struct{}{}
}

func (s Set[T]) ToSlice() []T {
	r := []T{}
	for k := range s {
		r = append(r, k)
	}

	return r
}

func main() {
	intSet := Set[int]{}
	intSet.Add(20)
	intSet.Add(500)
	fmt.Printf("%+v\n", intSet.ToSlice())

	strSet := Set[string]{}
	strSet.Add("hello")
	fmt.Printf("%+v\n", strSet.ToSlice())
}

make vs new

// make returns run-time initialised instance of the required type
// make can be used to initialize slices, maps, channels
mp := make(map[string]string, 10 /* size */)

Important: make cannot be used to initialise arrays. E.g. make([10]int, 10) is NOT valid.

// new is used to zero-initialise structs i.e. all fields are zero-initialised
// new returns a pointer to the initialised instance i.e. *User
user := new(User)

// the above is same as below (recommended)
user := &User{}

Range

myNumbers := []int{12, 23, 34, 45, 56, 67, 78}
for index, num := range myNumbers {
  fmt.Printf("%d %d\n", index, num)
}
for name, nation := range nationality {
  fmt.Println(name, nation)
}

Pointers

var a int = 30
var b *int = &a

fmt.Println("a:", a)    // 30
fmt.Println("b:", b)    // 0xc4200120c8
fmt.Println("*b:", *b)  // 30 i.e. dereferenced value

*b = 10                 // dereference and change value
fmt.Println("a:", a)    // 10
fmt.Println("b:", b)    // 0xc4200120c8
fmt.Println("*b:", *b)  // 10

Closures

func adder() func(int) int {
  sum := 0
  return func(x int) int {
    sum += x
    return sum
  }
}

func main() {
  sum := adder()

  for i := 1; i <= 5; i++ {
    fmt.Println(sum(i))
  }
}

/* 1 3 6 10 15 */
func fibs() func() int {
  val1 := 1
  val2 := 1
  tmp := 0

  return func() int {
    tmp = val1 + val2
    val1 = val2
    val2 = tmp

    return val2
  }
}

/* 1 2 3 5 8 13 21 34 55 89 */

Structs

type Car struct {
  brand string
  year uint64
  electric bool
}

Note: A struct field is only exported outside the package if it Begins with a Capital Alphabet. In the above Car Struct, none of the Fields are exported, i.e. all fields are private.

/* quicker approach */
myCar2 := Car{"Honda", 2010, true}

/* more explicit */
myCar := Car{
  brand:    "toyota",
  year:     2005,
  electric: false,
}

/* calling the attributes */
fmt.Println(myCar.brand, myCar2.electric)
Anonymous Structs
Employee := struct {
    Name string
    FullTime bool
}{
    Name: "Random user",
    FullTime: false,
}
Struct methods

There are two types of methods in Go

  • Value Receivers - They Get the values but cannot modify them
  • Pointer Receivers - They Set the values through pointers
type Employee struct {
  Name string
  Salary uint
}

/* value receiver methods */
func (e Employee) Display() {
  fmt.Printf("Employee:: Name: %v, Salary: %v\n", e.Name, e.Salary)
}

func (e Employee) Repr() string {
  return fmt.Sprintf("Employee:: Name: %v, Salary: %v", e.Name, e.Salary)
}

/* pointer receiver methods */
func (e *Employee) UpdateSalary(newSalary uint) {
  e.Salary = newSalary
}

func main() {
  e := Employee{
    Name: "Sana Ullah",
    Salary: 30000,
  }

  e.Display()
  fmt.Println(e.Repr())

  e.UpdateSalary(40000)
  e.Display()
}
Struct embedding
type Position struct {
	X float64
	Y float64
}

func (p *Position) Move(x, y float64) {
	p.X += x
	p.Y += y
}

func (p *Position) Teleport(x, y float64) {
	p.X = x
	p.Y = y
}

type Player struct {
	Name string
	*Position
}

func NewPlayer(name string) *Player {
	return &Player{
		Name:     name,
		Position: &Position{0, 0},
	}
}

func main() {
	player := NewPlayer("P One")
	fmt.Printf("position: %v\n", player.Position)

	player.Move(10.5, 20.5)
	fmt.Printf("position: %v\n", player.Position)
}
type SpecialPosition struct {
	Position
}

func (sp *SpecialPosition) SpecialMove(x, y float64) {
	sp.X += x * x
	sp.Y += y * y
}

type Enemy struct {
	*SpecialPosition
}

func NewEnemy() *Enemy {
	return &Enemy{
		SpecialPosition: &SpecialPosition{},
	}
}

func main() {
	enemy := NewEnemy()

	// use position methods
	enemy.Move(10, 40)

	// use special position methods
	enemy.SpecialMove(30, 50)

	fmt.Printf("position: %v\n", enemy.Position)
}
Methods on Primitive Types

Go enforces the rule that the Data Type and all its methods must be defined in the same package. Due to this rule we cannot directly implement methods on Primitive Data Types. However, we have the option to derive custom data types from primitive data types

type str string

func (s str) ToUpperCase() string {
  normalString := string(s)
  return strings.ToUpper(normalString)
}

func main() {
  s := str("This is a mixed case strinG")
  fmt.Println(s.ToUpperCase())
}

Interfaces

Go doesn’t have Generics / Templates like C++. Go is strongly Typed so we also don’t have the option of Duck Typing. The solution is to use Interfaces. An interface details a collection of methods.

package main

import (
  "fmt"
)

type Employee struct {
  age int
}

func (e Employee) Display() string {
  return fmt.Sprintf("Name: %v\n", e.age)
}

type Vine struct {
  vintage int
}

func (v Vine) Display() string {
  return fmt.Sprintf("Vintage: %v\n", v.vintage)
}

type Displayable interface {
  Display() string
}

func DisplayItem(i Displayable) {
  fmt.Print(i.Display())
}

func main() {
  items := []Displayable{
    Employee{30},
    Vine{1960},
  }

  for _, item := range items {
    DisplayItem(item)
  }
}
Combining interfaces
type Writer interface {
  Write(string) error
}

type Reader interface {
  Read() string
}

// this interface will include all methods of Writer and Reader interface
type ReadWriter interface {
  Writer
  Reader
}

Enums

Simple numeric enums
package direction

type Direction uint

const (
  LEFT Direction = iota
  RIGHT
  UP
  DOWN
)
package entity

type Entity struct {
  X uint
  Y uint
}

func (e *Entity) Move(d direction.Direction) error {
  /***/
}
Safer enums
type Direction struct {
  value string // private value, cannot be changed from outside
}

// ideally, enum types would be const. But, Go doesn't allow struct instances to
// be const.
var (
  Unknown = Direction{""} // sentinel value
  Up      = Direction{"Up"}
  Down    = Direction{"Down"}
  Left    = Direction{"Left"}
  Right   = Direction{"Right"}
)

func DirectionFromString(value string) (Direction, error) {
  switch value {
  case Up.value:
    return Up, nil

  case Down.value:
    return Down, nil

  case Left.value:
    return Left, nil

  case Right.value:
    return Right, nil

  default:
    return Unknown, fmt.Errorf("invalid direction '%s'", value)
  }
}

Note: Sentinel values are placeholder values which can be used to denote invalid states. Note that in this example it is the first value in enum.

Error Handling

package main

import (
  "fmt"
  "errors"
)

func task(num *int) error {
  if *num == -1 {
    return errors.New("Cannot process -1")
  }
  *num = *num * *num * *num
  return nil
}

func main() {
  num := 4
  err := task(&num)

  if err != nil {
    fmt.Println(err)
  }

  fmt.Println(num)
}

Note: error is simply an interface which looks like this

type error interface {
	Error() string
}
Custom errors
type APIError struct {
	error      string
	statusCode int
}

func NewAPIError(status int, message string) APIError {
	return APIError{message, status}
}

/* implementing this method satisfies the Error interface */
func (err APIError) Error() string {
	return err.error
}

func (err APIError) Status() int {
	return err.statusCode
}

func main() {
	err := NewAPIError(422, "Unauthorized")
	fmt.Printf("error: %s, status: %d\n", err.Error(), err.Status())
}

Panic and recover

func main() {
  // handle potectial panics on the current goroutine
  defer func() {
    if err := recover(); err != nil {
      fmt.Printf("panic inside go routing: %v\n", err)
    }
  }()

  panicAction()
}

func panicAction() {
  panic("panic happened ...")
}

Sorting

package main

import (
  "fmt"
  "sort"
)

func main() {
  nums := []int{23, 45, 21, 87, 45, 82, 17}
  sort.Ints(nums)
  fmt.Println(nums)

  strings := []string{"bravo", "alpha", "delta", "charlie"}
  sort.Strings(strings)
  fmt.Println(strings)

  flag := sort.IntsAreSorted(nums)
  fmt.Println(flag)

  flag = sort.StringsAreSorted(strings)
  fmt.Println(flag)
}

/*
[17 21 23 45 45 82 87]
[alpha bravo charlie delta]
true
true
*/

Go also provides a way to do custom sorts. Following code sorts by length an array of Strings

package main

import (
  "fmt"
  "sort"
)

type Data []string

func (d Data) Len() int {
  return len(d)
}

func (d Data) Swap(a, b int) {
  d[a], d[b] = d[b], d[a]
}

func (d Data) Less(a, b int) bool {
  return len(d[a]) < len(d[b])
}

func main() {
  msgs := []string{"This is", "a", "messagess"}
  sort.Sort(Data(msgs))
  fmt.Println(msgs)
}

Defer

Defer is used to postpone the execution of instructions till either

  • End of the current function (not block)
  • Error in the function

This means the deferred statement inside a function will be executed when the entirety of the function is executed OR the program runs into errors during execution of the function.

This is useful when resources need to be released no matter the successful execution of the function or otherwise, e.g. closing connections to the DB, closing open files, releasing threads etc.

func foo() {
  fmt.Println("Deferred Function")
}

func main() {
  defer foo()

  for i := 0; i <= 100; i++ {
     fmt.Print(i, "\t")
  }
}

In the above example, the program will count up to 100 and then run the foo function.

Example
func main() {
  {
    defer fmt.Println("One")
  }

  fmt.Println("Two")
  fmt.Println("Three")
}

// Two
// Three
// One

Note: The defer statement will execute at the end of the main function and NOT at the end of its block.

Generics

func SayHello[C any](user C) {
	fmt.Printf("Hello, %v\n", user)
}

func main() {
	SayHello("Admin")
	SayHello(100)
}
import (
	"encoding/json"
	"fmt"
	"os"
)

type APIOkResponse[T any] struct {
	Success bool `json:"success"`
	Data    T    `json:"data"`
}

type APIErrorResponse struct {
	Success    bool   `json:"success"`
	Error      string `json:"error"`
	StatusCode int    `json:"statusCode"`
}

func NewErrorResponse(status int, error string) APIErrorResponse {
	return APIErrorResponse{
		Success:    false,
		Error:      error,
		StatusCode: status,
	}
}

func NewOkResponse[T any](data T) APIOkResponse[T] {
	return APIOkResponse[T]{
		Success: true,
		Data:    data,
	}
}

type HelloResponse struct {
	Message string `json:"message"`
}

func OkResponseExample() {
	// generic type is inferred from arguments
	okRes := NewOkResponse(HelloResponse{
		Message: "hello world",
	})

    encoded, err := json.Marshal(okRes)
	if err != nil {
		fmt.Fprintf(os.Stderr, err.Error())
		return
	}
	fmt.Printf("%s\n", encoded)
}

func ErrorResponseExample() {
	errRes := NewErrorResponse(401, "You are not authorized")
	encoded, err := json.Marshal(errRes)
	if err != nil {
		fmt.Fprintf(os.Stderr, err.Error())
		return
	}
	fmt.Printf("%s\n", encoded)
}

Time execution

func main() {
	start := time.Now()
	defer func() {
		fmt.Printf("elapsed: %v\n", time.Since(start))
	}()

	fmt.Println("doing some work...")
	time.Sleep(time.Second * 3)
}

Time execution

func main() {
	start := time.Now()
	defer func() {
		fmt.Printf("elapsed: %v\n", time.Since(start))
	}()

	fmt.Println("doing some work...")
	time.Sleep(time.Second * 3)
}

Context

Storing value in a context
package main

import (
	"context"
	"errors"
	"fmt"
	"os"
)

type MyContextKey struct{}

func performAction(ctx context.Context) error {
	// by default, the returned value is of type 'any'. Here
	// we cast it to an 'int' type.
	value, ok := ctx.Value(MyContextKey{}).(int)
	if !ok {
		return errors.New("invalid value in context")
	}

	fmt.Printf("Context value: %v\n", value)
	return nil
}

func main() {
	ctx := context.Background()
	ctx = context.WithValue(ctx, MyContextKey{}, 300)

	if err := performAction(ctx); err != nil {
		fmt.Fprintf(os.Stderr, "error: %s\n", err.Error())
		os.Exit(1)
	}
}

Note: Strings etc. can also be used as context keys. However, it is far more efficient to use empty structs as context keys.

Cancelling contexts
import (
	"fmt"
	"time"
)

const (
	API_DELAY       = time.Second * 3
	REQUEST_TIMEOUT = time.Second * 5
)

type User struct {
	Id    int
	Email string
}

type UserWithError struct {
	User
	Error error
}

func fetchUser(id int) (User, error) {
	time.Sleep(API_DELAY)
	user := User{
		Id:    id,
		Email: "user@site.com",
	}

	return user, nil
}

func fetchUserWithTimeout(userId int, timeout time.Duration) (User, error) {
	userChan := make(chan UserWithError)
	defer close(userChan)

	go func() {
		user, err := fetchUser(userId)
		userChan <- UserWithError{user, err}
	}()

	select {
	case <-time.After(timeout):
		return User{}, fmt.Errorf("request timeout")

	case result := <-userChan:
		return result.User, result.Error
	}
}

func main() {
	start := time.Now()
	defer func() {
		fmt.Printf("elapsed: %v\n", time.Since(start))
	}()

	userId := 50
	user, err := fetchUserWithTimeout(userId, REQUEST_TIMEOUT)
	if err != nil {
		fmt.Printf("error: %s\n", err.Error())
		return
	}

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

Note: We don’t always need cancelling contexts. We can also achieve similar effect by doing the following.

select {
case <-time.After(time.Second * time.Duration(timeoutSeconds)):
	return User{}, fmt.Errorf("request timeout")

case result := <-userChan:
	return result.User, result.Error
}

Conditional Compilation

// File: main.go
package main

import (
	"fmt"
)

func main() {
	message := Greet()
	fmt.Println(message)
}

Note: If no tags are provided during build, the compilation will fail because the compiler is not sure which of the two following modules to use.

// File: greet_dev.go

//go:build dev
// +build dev
package main

func Greet() string {
	return "Greetings from dev flag"
}

Note: The above module will only be used if dev tag is provided during compilation.

// File: greet_prod.go

//go:build prod
// +build prod
package main

func Greet() string {
	return "Greetings from prod flag"
}

Note: The above module will only be used if prod tag is provided during compilation.

# run in dev mode
$ go run -tags dev .

# build in dev mode
$ go build -tags dev .

# build for production
$ go build -tags prod .