Go Concepts

Last updated on
15 min read

Table of Contents

Channels

  • Channels are used to communicate between goroutines. It’s a technique that allows one goroutine to send data to another.
  • By default a channel is bidirectional, i.e., goroutines can send and receive data over the same channel.
  • Create a channel of type int ch := make(chan int)
  • A simple example of sending and receiving via channels:
ch := make(chan int)

go func(){
  ch <- 10
}()

fmt.Println(<-ch)
  • Channel Types

    • Unbuffered channel (Synchronous communication)
      • Sending is blocked until receiver is ready.
      • Receiving is blocked until sender sends data.
      • ch = make(chan int)
      • This type of channel is beneficial where data sent by sender is immediately received by the receiver.
      • They can lead to deadlocks if not handled correctly.
    • Buffered channel (Asynchronous communication)
      • Sending is blocked when buffer is full.
      • Receiving is blocked when buffer is empty.
      • ch = make(chan int, 2)
      • This type of channel is beneficial in scenarios where sender sends data in bursts, while receiver processes it at different pace.
  • Channel direction can be restricted to improve safety:

    • Send only channel
      • func sendData(ch chan<- int){}
    • Receive only channel
      • func receiveData(ch <-chan int){}
  • Use close keyword to close a channel: close(ch). Only sender should close the channel.

  • Attempting to send data on a closed channel will lead to panic.

  • Receivers can check if the channel is closed using comma-ok idiom:

 value, ok := <-ch

 if !ok {
  fmt.Println("Channel is closed")
 }
  • Range keyword can be used to receive data in a loop:
for val := range ch{
  fmt.Println(val)
}
  • The loop blocks when waiting for values, and only exits when the channel is closed by the sender.
  • Channels are reference types. Passing a channel copies the reference. But the copy points to the same underlying channel. That is why pointers are not required in case of channels.
  • If a goroutine is waiting on a channel that no one ever writes, that will lead to memory leak.
  • Use cap(ch) to get the buffer capacity of the channel.
  • Use len(ch) to get the current number of items in the channel.

General

  • Best quick reference guide for go - https://github.com/karanpratapsingh/learn-go
  • Learning go with Tests - https://quii.gitbook.io/learn-go-with-tests
  • go.mod specifies go module, contains go version and dependencies
  • go.sum contains checksums (hashes) of all direct and indirect module dependencies.
  • Functions are first class citizens. That means, just like datatypes, functions can be assigned to variables, passed as function arguments, and returned from functions etc
  • Value types (int, float, string, bool, structs) - When you pass it to a function it will create a copy. So if you modify, it won’t affect the original
  • Reference types in go - slices, maps, channels, pointers, functions - When you pass it to a function it pass the original memory address and any changes that you do will affect the original
  • Garbage collection - go uses mark and sweep algorithm. In the mark phase, gc marks all the reachable objects, in the sweep phase it removes all the unreachable objects
  • In a package make function private by using the first letter of the function name lowercase, and make the first letter uppercase to make it public.
  • Statically typed language
  • When you say package main - it means it’s an executable. If you name it anything else, it will be considered as a library, which you will import in other projects.
  • If a variable or function starts with uppercase letter then it is exported and can be used by other packages. If it is lowercase then cannot be accessed from outer packages.
  • func main() is the first function executed when the program runs.
  • Defer keyword is used where you want to perform certain operations before the function returns, regardless of whether there was any error. Suppose you have a DB connection object and you clear it. The added benefit here is that you can the defer statement anywhere and it will executed only at the end of the function. So you can add it anywhere and forget about it. If there are multiple defer statements then they are executed in reverse order (LIFO). Even in case of a panic, the defer functions are executed. It’s similar to destructor (in classes) with the additional benefit that, if for some reason, the application panics, the deferred statements will still be called.
  • go.mod contains the module name, all dependencies, go version etc.
  • pprof - a tool for visualization and analysis of profiling data.
  • go doesn’t support inheritance.It supports composition.
  • Difference between json.Marshall and json.NewEncoder is, both are used to convert to JSON data. The difference is marshall is used for string data and NewEncoder is used for streaming data.
  • Fiber is a web framework similar to expressJS.
  • panic is raised by the program itself when unexpected error occurs or the programmer throws to handle exceptions.
  • recover - recovers from a panic and stops the program from aborting. Should always be called in defer function. For example, if a client connection is lost, the server would want to close that connection, and continue with other clients, instead of entirely crashing the server.
  • os package is used to receive signal from os. Also provides functionality to build communication between os and the program. os.Args (gets the arguments passed when running the binary), os.Stdin, os.Stdout (for keyboard interactions and printing output), os.Exit, os.Environ and for getting and setting environment variables.
  • To list the number of goroutines running at a moment, you can use runtime package to get the count of goroutines. It’s runtime.NumGoroutine()
  • mutex is used to prevent the parts of code which modify shared resources from getting accessed at the same time. Use mutex.Lock() and Unlock functions to do this.
  • & is for memory address retrieval and * is for dereferencing.
  • When a function parameter has empty interface, it means you can pass any kind of data, be it int, string, struct. For example Marshal function allows you to pass an empty interface.

Inbuilt

Datatypes

Basic Types
  • bool: represents a boolean value, either true or false
  • numeric types:
    • integers: signed (int) and unsigned (uint) integers of varying sizes (8, 16, 32, 64 bits)
    • floating-point numbers: float32 and float64
    • complex numbers: complex64 and complex128
  • string: represents a sequence of characters
Aggregate Types
Array
  • A collection of elements of the same type with a fixed length
var myArray [5]int // array declaration
var myArray [5]int = [5]int {1, 2, 3, 4, 5} // declaring array with default values
myArray[0] = 1 // Updating a value
for i, item := range myArray{ // Looping through array
	fmt.Println(i, item)
}
Struct
  • They are value type
  • A collection of fields of different types
  • Provides a way to create complex data structures by combining different types
type struct_name struct {
    /* variables */
}
Reference Types
slice
  • Slice is a dynamic array
  • Declaring a slice - var sliceArr []int
  • Declaring with some values - var sliceArr []int = []int{1, 2, 3}
  • Append data to slice - sliceArr = append(sliceArr, 4)
var x []int // declaring slice
var x = []int{1, 2, 3, 4, 5} // slice with default values
var x = make([]string, 0) // slice using make
x = append(x, val) // appending to a slice
pointer
  • a reference to the memory location of another variable
map
  • an unordered collection of key-value pairs
  • Declaring a map - var hashMap map[string]int - creates map with keys as string and value as int
  • Declaring with some values - var hashMap map[string]int = map[string]int{"idli": 2, "vada": 1}
var m map[string]int
var m = map[string]int{
	"a": 1,
	"b": 2,
}
var m = make(map[string]int) // Declaring using make
m["a"] = 3 // Updating a value of a map
m["a"] // accessing a value in a map
delete(m, "a") // deleting a key from map
for k, v := range m { // Iterating over a map
	fmt.Println(k, v)
}

c, found := m["key"] // Check if key exists
function
  • a first-class value representing an executable function
Interface
  • a collection of method signatures that a concrete type must implement
type Shape interface {
    Area() float64
    Perimeter() float64
}

type Rectangle struct {
    Width  float64
    Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

func main() {
    rect := Rectangle{Width: 5, Height: 3}
    var shape Shape
    shape = rect
    fmt.Println(shape.Area())
    fmt.Println(shape.Perimeter())
}
Other Types
  • chan: a channel for communicating between goroutines
  • unsafe.Pointer: a pointer without any type safety Here are some examples of declaring variables with different data types in Go:
var b bool = true                // boolean
var i int = 42                   // signed integer
var u uint = 42                  // unsigned integer
var f float32 = 3.14             // 32-bit floating-point number
var c complex128 = 1 + 2i        // 128-bit complex number
var s string = "hello"           // string
var a [3]int = [3]int{1, 2, 3}   // array of 3 integers
var p *int = &i                  // pointer to an integer
var sl []int = []int{1, 2, 3}    // slice of integers
var m map[string]int             // map with string keys and integer values
var x interface{} = 42           // empty interface can hold any value

Commands

  • go mod init project-name - initiates a go module contain the name and the version only.
  • go mod tidy - removes unused and adds used dependencies, cleans up the go.mod and go.sum files
  • go get package_name - to download and add this as a dependency to the current project
  • go run . or go run main.go

Packages

fmt

  • Printf - “Print Formatter”: this function allows you to format numbers, variables and strings into the first string parameter you give it
  • Print - “Print”: This cannot format anything, it simply takes a string and prints it
  • Println - “Print Line”: same as Print() but appends a newline character \n at the end.
  • Print, Println, Printf writes output directly to standard output, typically used to print to console
  • Fprint, Fprintln, Fprintf writes output to io.Writer. Useful to write to files, network connections, or custom writers. Basically takes Fprintf(write, string, format)
  • Sprint, Sprintln, Sprintf - return the string and will not write it anywhere
  • Format verbs in go -
    • %s - string
    • %d - integer
    • %f - decimal format
    • %v – The default format for the value.
    • %+v – Adds field names for structs.
    • %#v – Go syntax representation of the value.
    • %T – Prints the type of the value.
    • %% – A literal percent sign (%).
    • %t - boolean
    • %b - binary values
    • %o - octal value
    • %p - memory address in hexadecimal
  • log.Println vs fmt.Println
    • fmt.Println - general purpose logging to standard output
    • log.Println - used for logging errors, debug messages, includes timestamps

net

  • net.DialTimeout(network, address, timeout). Returns connection and error object

net/http

  • http.Handle vs http.HandleFunc
    • In case of handle you must implement the http.Handler interface, (i.e., it must have a ServeHTTP(w http.ResponseWriter, r *http.Request) method).
    • In case of handleFunc, it must have a func(w http.ResponseWriter, r *http.Request) function

time

  • time.Sleep(1 * time.Second) - sleeps for a second
  • t := time.Now()
  • constants
    • Nanosecond
    • Microsecond
    • Millisecond
    • Second
    • Minute
    • Hour

testing

  • testing.B is for benchmarking, testing.T is assertive testing, testing.F is for fuzzy testing
  • Benchmarking solutions
    • Using time
startTime := time.Now()

output := functionNameToBeProcessed(500)

endTime := time.Now()
executionTime := endTime.Sub(startTime)
- Using testing.B
func BenchmarkDivisorCount(b *testing.B) {
	for i := 0; i < b.N; i++ {
		functionNameToBeProcessed(500)
	}
}

bufio

Bufio Reader
  • NewReader - Reading text using NewReader from standard input
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter text: ")
// Read input until a newline
text, _ := reader.ReadString('\n')
// Print the input text
fmt.Println("You entered:", text)
  • NewReader - Reading file using NewReader from file
// Open a file for reading
file, err := os.Open("example.txt")
if err != nil {
	fmt.Println(err)
	return
}
defer file.Close()

// Create a buffered reader
reader := bufio.NewReader(file)

// Read line by line
for {
	line, err := reader.ReadString('\n')
	if err != nil {
		// Check for end of file
		if err.Error() == "EOF" {
			break
		}
		fmt.Println("Error reading file:", err)
		return
	}
	fmt.Print(line) // Print each line
}
  • NewScanner - Reading text using NewScanner from standard input
scanner := bufio.NewScanner(os.Stdin)

fmt.Print("Enter text: ")

// Read the input line-by-line
if scanner.Scan() {
	text := scanner.Text()
	fmt.Println("You entered:", text)
}

// Check for any scanning error
if err := scanner.Err(); err != nil {
	fmt.Println("Error reading input:", err)
}
  • NewScanner - Reading text using NewScanner from file
// Open the file for reading
file, err := os.Open("example.txt")
if err != nil {
	fmt.Println("Error opening file:", err)
	return
}
defer file.Close()

// Create a scanner to read the file line-by-line
scanner := bufio.NewScanner(file)

// Loop through the file and print each line
for scanner.Scan() {
	fmt.Println(scanner.Text())
}

// Check for any scanning errors
if err := scanner.Err(); err != nil {
	fmt.Println("Error reading file:", err)
}
Bufio Writer
  • NewWriter - Writing to a file
 // Create a file for writing
file, err := os.Create("output.txt")
if err != nil {
	fmt.Println("Error creating file:", err)
	return
}
defer file.Close()

// Create a bufio.Writer with a buffer size of 4096 bytes
writer := bufio.NewWriter(file)

// Write some text to the buffer
writer.WriteString("Hello, World!\n")

// Flush the buffer to ensure everything is written to the file
writer.Flush()

fmt.Println("Data written to file.")

reflect

JSON Serialization & Deserialization

When working with JSON in Go, the encoding/json package internally uses reflection to map JSON fields to struct fields dynamically.

package main

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

type Person struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

func main() {
	p := Person{Name: "Alice", Age: 30}

	// Convert struct to JSON using reflection internally
	jsonData, _ := json.Marshal(p)
	fmt.Println(string(jsonData)) // Output: {"name":"Alice","age":30}

	// Decode JSON dynamically using reflection
	var result map[string]interface{}
	json.Unmarshal(jsonData, &result)

	for key, value := range result {
		fmt.Println("Field:", key, "Value:", value, "Type:", reflect.TypeOf(value))
	}
}
Dynamic Struct Field Validation (e.g., Form Validation)

You can inspect struct tags and validate user inputs dynamically.

package main

import (
	"errors"
	"fmt"
	"reflect"
)

type User struct {
	Name  string `validate:"required"`
	Email string `validate:"required"`
	Age   int    `validate:"optional"`
}

func validateStruct(s interface{}) error {
	v := reflect.ValueOf(s)
	t := reflect.TypeOf(s)

	for i := 0; i < v.NumField(); i++ {
		field := t.Field(i)
		tag := field.Tag.Get("validate")
		value := v.Field(i)

		if tag == "required" && value.Interface() == reflect.Zero(field.Type).Interface() {
			return errors.New(field.Name + " is required")
		}
	}
	return nil
}

func main() {
	user := User{Name: "John"}
	err := validateStruct(user)
	if err != nil {
		fmt.Println("Validation Error:", err) // Output: Validation Error: Email is required
	} else {
		fmt.Println("User is valid")
	}
}
ORM (Object-Relational Mapping)

ORMs like GORM use reflect to map Go structs to database tables dynamically.

package main

import (
	"fmt"
	"reflect"
)

type User struct {
	ID    int
	Name  string
	Email string
}

func generateInsertQuery(data interface{}) string {
	t := reflect.TypeOf(data)
	v := reflect.ValueOf(data)

	columns := ""
	values := ""

	for i := 0; i < t.NumField(); i++ {
		if i > 0 {
			columns += ", "
			values += ", "
		}
		columns += t.Field(i).Name
		values += fmt.Sprintf("'%v'", v.Field(i).Interface())
	}

	return fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s);", t.Name(), columns, values)
}

func main() {
	user := User{ID: 1, Name: "Alice", Email: "alice@example.com"}
	query := generateInsertQuery(user)
	fmt.Println(query)
}
Dynamic RPC (Remote Procedure Call) Handlers

Frameworks like gRPC and net/rpc use reflection to dynamically invoke methods over the network.

package main

import (
	"fmt"
	"reflect"
)

type Service struct{}

func (s Service) SayHello(name string) string {
	return "Hello, " + name
}

func callMethod(instance interface{}, methodName string, args ...interface{}) {
	v := reflect.ValueOf(instance)
	method := v.MethodByName(methodName)

	in := make([]reflect.Value, len(args))
	for i, arg := range args {
		in[i] = reflect.ValueOf(arg)
	}

	result := method.Call(in)
	fmt.Println(result[0].Interface()) // Output: Hello, Alice
}

func main() {
	service := Service{}
	callMethod(service, "SayHello", "Alice")
}

fmt

fmt Reader
  • Scan
var input string
fmt.Print("Enter text: ")
fmt.Scan(&input)
fmt.Println("You entered:", input)
  • Scanf
var name string
var age int
fmt.Print("Enter your name and age: ")
fmt.Scanf("%s %d", &name, &age)
fmt.Printf("Name: %s, Age: %d\n", name, age)
Others
  • Difference between log and fmt. Log supports additional features like:
    • Added output time
    • Thread Safety
    • Easy to dump log information to form a log file
  • printf, fprintf, sprintf
    • printf is equivalent to writing fprintf(stdout, ...) and writes formatted text to wherever the standard output stream is currently pointing.
    • fprintf writes formatted text to the output stream you specify.
    • sprintf writes formatted text to an array of char, as opposed to a stream.

string

  • TrimSpace - name = strings.TrimSpace(name)
  • Convert from string to integer - strconv.Atoi()

context

net/http

  • If you pass nil as the second argument to http.ListenAndServe(), the server will use http.DefaultServeMux for routing.
  • Wildcard segments in a route pattern are denoted by an wildcard identifier inside {} brackets. Like this - /products/{productId}. And you can retrieve the path value in the handler using - product := r.PathValue("productId")
  • When route patterns overlap, Go’s servemux needs to decide which pattern takes precedent so it can dispatch the request to the appropriate handler. The rule for this is very neat and succinct: the most specific route pattern wins. The route pattern “/post/edit” only matches requests with the exact path /post/edit, whereas the pattern “/post/{id}” matches requests with the path /post/edit, /post/123, /post/abc and many more. Therefore “/post/edit” is the more specific route pattern and will take precedent.
  • HTTP method based routing - mux.HandleFunc("GET /products/{id}", snippetView)
  • Use WriteHeader to write a custom header for a status code - w.WriteHeader(201)
  • You can use status code constants to write a status code - w.WriteHeader(http.StatusCreated)
  • Send custom response headers using - w.Header().Add("key", "value")
  • Update a response header using - w.Header().Set("Content-Type", "application/json")
  • w.Header() has Add(), Set(), Del(), Get(), Values() methods supported
  • Anything that uses io.Writer interface can be used to write the response, for example
    • w.Write([]byte("Hello world"))
    • io.WriteString(w, "Hello world")
    • fmt.Fprint(w, "Hello world")
  • File Server to provide static files -
    fs := http.FileServer(http.Dir("/home/bob/static"))
    http.Handle("/static/", http.StripPrefix("/static", fs))
  • All incoming HTTP requests are served in their own goroutine. For busy servers, this means it’s very likely that the code in or called by your handlers will be running concurrently.

Hello world web server

package main

import (
	"log"
	"net/http"
)

func Home(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello World"))
}

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("/", Home)

	log.Print("Starting server on port 4000")

	err := http.ListenAndServe(":4000", mux)
	log.Fatal(err)
}

To be added

  • sync package - waitgroups

  • math package

  • log package

  • What do commands doc, clean, env, fix, list, run, test, tool, vet do?

  • go vet, gofmt usage

  • type conversions in go

  • type assertion

  • error handling and custom error handling

  • generics

  • io package. Reader and write interface

  • combining 2 or more interfaces into a new interface

  • Concrete type vs Interface type

  • change number of cores used by go

  • method sets

  • receiver functions

  • variadic functions