Yeah, it's not going to be easy

Search

Search IconIcon to open search

Tucker's Golang programming Ch23 ~ Ch26

Oct 29, 2023

This post is a translation of the original Korean post.


This text is a summary of chapters 23 to 26 of the book “Golden Rabbit: [ Tucker’s Golang programming ]”.

Ch23 ~ Ch26

  • Error Handling
  • GoRoutine and Concurrent Programming
  • Channels and Contexts
  • Creating a Word Search Program

# Error Handling

# Error type

1
2
3
type error interface {
	Error() string
}

Can be used as an error if you implement an Error() method that returns a string

# Generate an error

Create a user error

1
errors.New("Error Message")

Wrapping an error

  • Wrap an error to create a new error
1
2
3
fmt.Errorf("Error wrapping: %w", err)

if errors.As(err, &numError) { // Check the wrapped error type (is there any error in the wrapped error that can be converted to the numError type)

# panic

Ability to stop program flow (crash)

  • Terminate the program immediately to quickly see when something goes wrong
  • Can check the call stack (order of function calls)
1
2
3
4
5
// Declare the panic() function
func panic(interface{})

panic("Error message")
panic(fmt.Errorf("This is error num: %d", num))

recover()

Recovers a panic from terminating the program.

  • Returns the panic object that occurred
1
2
3
4
5
6
// Declare the recover() function
func recover() interface{}

if r, ok := recover().(net.Error); ok { // type checking is required to use the returned panic object
	fmt.Println("r is net.Error Type")
}

# Goroutine

Lightweight threads managed by the Go language

  • Used to execute functions or commands simultaneously
1
go function_call

Main Routine

Starts and ends with the main() function (also exits the program)

  • Subroutines created from the main() function also exit immediately when the main() function exits.

# WaitGroup

Used to wait for a group to exit from a goroutine

1
2
3
4
5
var wg sync.WaitGroup

wg.Add(3) // Set the number of tasks
wg.Done() // complete the task (decrement the number of tasks by 1)
wg.Wait() // wait for all tasks to finish

# How it works

Lightweight threads using OS threads

  • Allocate only one OS thread per CPU core and use it.
  • When there are no cores available, the goroutines wait until there are.
  • When a threaded goroutine enters a wait state (e.g. listening to the network), it swaps with the waiting goroutines.

Advantages.

No context switching costs (the CPU core doesn’t change threads, it just moves the goroutines)

# Concurrency programming

# Mutexes

Mutual exclusion

  • Controlling access to resources
1
2
3
4
var mutex sync.Mutex

mutex.Lock() // Acquire the mutex: other goroutines wait until the mutex is returned
defer mutex.Unlock() // Return the mutex

Disadvantages

  • No performance gains from concurrent programming > - Only one goroutine can access the shared resource
  • Potential for deadlocks > - Make sure it doesn’t deadlock and use it in a narrow scope.

# Resource management techniques

Manage resources so that goroutines do not access the same resource

  • How to divide the area
    • Allocate goroutines by e.g. file
  • How to divide roles
    • Using Channels.

# Channel and context

# Channels

Message queue for passing messages between goroutines

Create an instance

1
2
3
4
var messages chan string = make(chan string)

// create a channel with a buffer (size: 2)
var messages chan string = make(chan string, 2)

Using

1
2
3
4
5
// put data in
messages <- "this is a message"

// Subtract data
var msg string = <- messages

# range statement

Get all the values of the channel and escape if it’s closed

1
2
3
for n := range ch {
	...
}

# select statement

1
2
3
4
5
6
7
select {
case n := <-ch1:
	...
case n2 := <-ch2:
	...
case ...
}
  • Waiting on multiple channels simultaneously
  • Use with a for statement to continue processing data without exiting
  • Use inside a goroutine to wait on a channel while doing something else

time channel

  • time.Tick(): Creates a channel that sends a signal at regular time intervals cycle.
  • time.After(): Create a channel that sends a signal after a certain amount of time after.

# Producer Consumer Pattern

How to divide roles (conveyor belt system)

Put channels between goroutines(tasks) and pass the results of the tasks to the channels.

advantages

Instead of waiting for all tasks to finish before starting the next one. you can start your own task

# Context

Function provided by the context package to act as a task statement.

Context for canceling tasks

1
2
3
4
5
6
7
8
9
// Create a cancelable context
ctx, cancel := context.WithCancel(context.Background())
...
cancel() // signal the context's Done() channel
...
...
case <- ctx.Done():
	wg.Done()
	return

Context in which the task time was set

1
2
// Signal the context's Done() channel after the set time has passed
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)

Context with a specific value set

1
2
3
4
5
6
7
ctx := context.WithValue(context.Background(), "number", 9)
...

if v := ctx.Value("number"); v != nil {
	n := v.(int) // return type is an empty interface, type conversion required
	...
}

context wrapping

When creating a context, pass an already created context object as an argument.

context wrapping is possible

# Create a word search program

Structure

tucker-golang-programming-ch26.png

Code

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
package main

import (
	"bufio"
	"errors"
	"fmt"
	"os"
	"strconv"
	"strings"
	"sync"

	"path/filepath"
)

const linePrint = "------------------------------"

func main() {
	word, paths, err := input()
	if err != nil {
		panic(err)
	}

	var filePaths []string
	for _, path := range paths {
		matches, err := getFiles(path)
		if err != nil {
			fmt.Printf("File path is invalid. path: %s, err: %s\n", path, err)
			continue
		}
		filePaths = append(filePaths, matches...)
	}
	if len(filePaths) == 0 {
		fmt.Println("No matching files found.")
		return
	}

	resultCh := make(chan string, len(filePaths))
	f := finder{
		filePaths,
	}
	go f.find(word, resultCh)

	sb := strings.Builder{}
	for result := range resultCh {
		sb.WriteString(result)
	}
	fmt.Println(sb.String())
}

// finder has the file paths to look for
type finder struct {
	filePaths []string
}

// input return the file corresponding to the input path
func input() (string, []string, error) {
	if len(os.Args) < 2 {
		return "", nil, errors.New("empty word argument")
	}
	word := os.Args[1]
	if len(os.Args) < 3 {
		return "", nil, errors.New("empty filepath argument")
	}
	filePaths := os.Args[2:] // recognize wildcards as variable arguments when using them
	return word, filePaths, nil
}

// getFiles return a list of files corresponding to the path
func getFiles(path string) ([]string, error) {
	return filepath.Glob(path)
}

// find search for files in a filelist
func (f finder) find(word string, resultCh chan string) {
	var wg sync.WaitGroup
	for _, filePath := range f.filePaths {
		wg.Add(1)
		go func(filePath string) {
			defer wg.Done()
			s, err := search(word, filePath)
			if err != nil {
				resultCh <- err.Error()
			}
			if s != "" {
				resultCh <- s
			}
		}(filePath)
	}
	wg.Wait()
	close(resultCh)
}

// search open a search file to search for lines matching a word, and generate an output string
func search(word string, filePath string) (string, error) {
	// open file
	file, err := os.Open(filePath)
	if err != nil {
		return "", fmt.Errorf("file not found. err: %w", err)
	}
	defer func() {
		if err := file.Close(); err != nil {
			fmt.Printf("Error closing file: %s, err: %s\n", filePath, err)
		}
	}()

	// find word in file
	result := find(word, file)

	// print string
	sb := strings.Builder{}
	sb.WriteString(file.Name()) // file name
	sb.WriteRune('\n')
	sb.WriteString(linePrint)
	sb.WriteRune('\n')
	sb.WriteString(result)
	sb.WriteString(linePrint)
	sb.WriteRune('\n')
	sb.WriteRune('\n')

	return sb.String(), nil
}

// find lines matching a word in a file and generate a string
func find(word string, file *os.File) string {
	sb := strings.Builder{}
	sc := bufio.NewScanner(file)
	for lineNum := 1; sc.Scan(); lineNum++ {
		text := sc.Text()
		if strings.Contains(text, word) {
			sb.WriteRune('\t')
			sb.WriteString(strconv.Itoa(lineNum))
			sb.WriteRune('\t')
			sb.WriteString(text)
			sb.WriteRune('\n')
		}
	}
	if sb.Len() == 0 {
		sb.WriteString("not found")
		sb.WriteRune('\n')
	}
	return sb.String()
}

# References