Practise on Golang
1. Introduction
This doc records down my practise on Golang from work. It includes the problems I met when I swtiched to Golang as a 3 years experienced developer.
2. Prepare develoment environment
2.1. Install Go
Update ubuntu
sudo apt-get -y update
Troubleshooting: invliad PPA error
- In my case it is something related with “yarn”.
- So delete that PPA. see: How to Remove or Delete PPA in Ubuntu Linux
ls /etc/apt/sources.list.d sudo rm -i /etc/apt/sources.list.d/yarn.list sudo rm -i /etc/apt/sources.list.d/yarn.list.save
- In my case it is something related with “yarn”.
- Download go install package
- Go to the download page and download the binary release suitable for your system.
Extract the archive file
sudo tar -C /usr/local -xzf go1.16.6.linux-amd64.tar.gz
Make sure
go/binis in PATH
export PATH=$PATH:/usr/local/go/bin
Check installed go version and its env
go version go env
- Go to the download page and download the binary release suitable for your system.
2.2. (optional) Set Go proxy for development in China
- What is Go proxy?
Set proxy according to your location
go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct # Or other proxy go env -w GOPROXY=https://goproxy.io/zh/,direct
Test proxy setting works
time go get golang.org/x/tourTo unset proxy
go env -u GOPROXY # Now it should show default one: go env | grep proxy # GOPROXY="https://proxy.golang.org,direct"
2.3. Setup project
- Create folder -tester
Cd into
-tester, then initialize the project as go module
go mod init loadtest go get -u github.com/aws/aws-sdk-go/...
Test aws configuration(
~/.aws/config) since this work is related with using AWS SDK.
package main import ( "fmt" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" ) func main() { sess, err := session.NewSession(&aws.Config{ Region: aws.String("us-east-1")}, ) if err != nil { fmt.Println("session error:", err) } fmt.Printf("using session: %v\n", sess) }- Then,
go run main.goto see if it works. We will implement different packages(
.goand*_test.goin one folder) under thisloadtestmodule(the whole code repository). In one package, we use other packages like:
package main import ( "fmt" "loadtest/aws" "loadtest/cmd" "loadtest/tools" ... )Notice: you could not import main package into other package. So, keep main package simple.
2.4. About Golang SDK
2.4.1. The Go AWS SDK convention
- In AWS Go SDK document, it uses a lot of pointers. And Go SDK provides helper functions which do the convertion between values to pointers and pointers to values. See: Value and Pointer Conversion Utilities.
Examples
var strPtr *string // Without the SDK's conversion functions str := "my string" strPtr = &str // With the SDK's conversion functions strPtr = aws.String("my string") // Convert *string to string value str = aws.StringValue(strPtr) var strPtrs []*string var strs []string = []string{"Go", "Gophers", "Go"} // Convert []string to []*string strPtrs = aws.StringSlice(strs) // Convert []*string to []string strs = aws.StringValueSlice(strPtrs)
2.4.2. Unit test with AWS SDK
- In Golang we use interface to decouple the dependencies of component. From AWS SDK, it provides a lot of useful interfaces for us to use.
- Then, We just use those interface instead of concrete type in our struct. So later, we could mock API easily.
3. The practise from code
The Golang code I am writing is a loadtest script:
- It utilizes existing external code, a javascript script tester which does end-to-end test.
- It launches multiple instance of such testers.
- It parses the stdout and stderr from each execution of E2E test.
- It should monitor the execution status of loadtest, such as how many launched, executing, finished.
- It should cancel all remaining tester from ctrl-C.
Basically it is a stander master-worker structure. This section records down the problems I met and Googled.
3.1. How to get project’s root from .git?
import "os/exec"
func getCurrentProjectRoot() (string, error) {
cmdOut, err := exec.Command("git", "rev-parse", "--show-toplevel").Output()
if err != nil {
return "", errors.New("you are not in a project")
}
return strings.TrimSpace(string(cmdOut)), nil
}
3.2. How to execute external program?
3.2.1. Prepare the cmd to execute
import "os/exec"
func PrepareTester(workerVersion, jsonFile string) *exec.Cmd {
rootPath, err := getCurrentProjectRoot()
if err != nil {
panic(err)
}
// cmd used on command-line: node index.js -j input_clouddash_dev.json -w Worker.3100 -m C -p zeus
app := "node"
arg0 := "index.js"
...
cmd := exec.Command(app, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)
cmd.Dir = rootPath + "/code"
return cmd
}
3.2.2. Execute exec result in real-time
In this work, the tester’s execution will take for a while, and it is neccessary to view the complete output on the terminal while saving its output into files.
- We need to save output from stderr into a file.
- We need to combine stdout and stderr to save it together into a file while displaying it on console.
- Because there are two data streams: stderr, and stderr + stdout. We use two goroutine to handle them.
- We start exec, wait it’s done.
Code does write stderr, stdout into files
cmd := cmd.PrepareTester(workerVersion, jsonFile)
cmdStdout := make(chan string, 10)
outf, _ := os.Create(filepath.Join(logFolder, fmt.Sprintf("%v-out.txt", id)))
errf, _ := os.Create(filepath.Join(logFolder, fmt.Sprintf("%v-err.txt", id)))
defer outf.Close()
defer errf.Close()
var wg sync.WaitGroup
wg.Add(1)
// write exec's stdout into stdout file
go func() {
readAndWriteToFiles(stdoutIn, cmdStdout, []*os.File{outf})
wg.Done()
}()
wg.Add(1)
// write exec's stderr into stderr file as well as into stdout file.
go func() {
readAndWriteToFiles(stderrIn, cmdStdout, []*os.File{outf, errf})
wg.Done()
}()
wg.Wait()
cmd.Wait()
- We also write both stderr and stdout into a channel, then we could read from that channel and do further process for exec’s output. For example, print out on console or save them for further process.
A helper function: read from io.Reader and write them to os.File and to chan string
func readAndWriteToFiles(r io.Reader, ch chan<- string, fs []*os.File) { buf := make([]byte, 512) for { n, err := r.Read(buf[:]) if n > 0 { d := buf[:n] for _, f := range fs { f.Write(d) } ch <- string(d) } if err != nil { if err == io.EOF { break } } } }
3.3. Process JSON
I would say the only downside of using comparing with Javascript is it is not very convenient to process JSON. In Golang, JSON object is represent as a combination of map[string]interfaces{} and []interface{}.
3.3.1. Encod JSON to bytes
For example, Golang’s
net/httpmodule needs the body of http to be bytes, so we need to encode JSON object into bytes.
client := &http.Client{ Timeout: time.Millisecond * 2500, } contract := map[string]interface{}{ "workerInfo": map[string]string{"workerVersion": workerVersion}, "userInfo": map[string]string{"userId": userId}, "clientInfo": map[string]string{"clientId": "-tester", "clientVersion": "1.3.4"}, } body := map[string]interface{}{ "Contract": contract, } reqBody, _ := json.Marshal(body) req, _ := http.NewRequest("POST", url+"/init", bytes.NewBuffer(reqBody)) req.Header.Set("Content-Type", "application/json") req.Header.Set("x-ams-apiKey", apiKey) resp, err := client.Do(req) if err != nil { log.Println(err) return sessionResult, false } defer resp.Body.Close()- First
json.Marshal, thenbytes.NewBuffer.
- First
3.3.2. Decode byte to JSON object
For example, the response we got from http post contain the
type InitSessionResult struct { SessionId string Expiry int64 // in Millisecond ExpiryTime time.Time } resBody, err := ioutil.ReadAll(resp.Body) if err != nil { panic(err) } fmt.Println("InitSessionId response Status:", resp.Status) var sessionResult InitSessionResult err = json.Unmarshal(resBody, &sessionResult) if err == nil { sessionResult.ExpiryTime = time.Unix(sessionResult.Expiry/1000, 0) }- If we know the JSON response structure from which we will parse, it becomes easy: we just
json.Unmarshalit. And it will fill our struct autmatically based some rules (seeencoding/jsonfor detail).
- If we know the JSON response structure from which we will parse, it becomes easy: we just
- For cases where we don’t know the JSON response structure. (TODO)
3.3.3. Reference
- ref: The Go Blog: JSON and Go
- ref: Go by example: JSON
3.4. How to do something related with time
3.4.1. How to convert from Unix timestamp to time.Time
// Here, the timestamp I got is Millisecond time.Unix(*r.Timestamp/1000, 0)
func Unix(sec int64, nsec int64) Time
3.4.2. How to convert from time.Time to Unix timestamp
func (t Time) Unix() int64, the number of secondfunc (Time) UnixMilli, the number of milliseconds elapsed since January 1, 1970 UTCfunc (Time) UnixNano
3.4.3. How to format time
func fromTimeToStr(t time.Time, format string, asUTC bool) string {
if asUTC {
return fmt.Sprintf("%v", t.In(time.UTC).Format(format))
} else {
return fmt.Sprintf("%v", t.In(time.Local).Format(format))
}
}
- Where, the format is something like: “2006-01-02 15:04:05.99-0700” called layout.
- See Constants defined in time package
3.4.4. How to parse a string to time.Time
func Parse(layout, value string) (Time, error)
startAt, err := time.Parse("2006-01-02 15:04", startStr) if err != nil { log.Fatal("time format is not correct, exit...") }func ParseInLocation(layout, value string, loc *Location) (Time, error)
3.4.5. How to format time
func formatTime(t time.Time) string {
// setUTC is a global variable control whether we want is a time relative to UTC
return fromTimeToStr(t, "2006-01-02 15:04:05.99-0700", setUTC)
}
func fromTimeToStr(t time.Time, format string, asUTC bool) string {
if asUTC {
return fmt.Sprintf("%v", t.In(time.UTC).Format(format))
} else {
return fmt.Sprintf("%v", t.In(time.Local).Format(format))
}
}
3.4.6. How to set duration
Set time duration for specific number, such as 30 seconds
var testerNoResponseLimit time.Duration = 30 * time.Second
Set time duration between one timestamp
begin := time.Now() // ...do something time.Since(begin)
Set time duration between two timestamp using
Sub
// use latest time - a timestamp set before time.Now().Sub(startAt)
From a time at one side of duration to get another time.
fiveMinsAgo := time.Now().Add(-5 * time.Minute)
How to multiple duration by integer?
Usually we got a variable as a number and we want to multiple it by unit of duration, such as seconds or mins. For a variable as number, we need to convert it to time.Duration explicitly.
start := time.Now().Add(-time.Duration(parsedInteger) * time.Minute)
3.4.7. How to postpone execution
// postpone is time.Duration time.Sleep(postpone)
3.4.8. How to repeated do something with time.Duration
// First setup a ticker
updateTicker := time.NewTicker(5 * time.Second)
go func() {
// stop it using defer
defer updateTicker.Stop()
for {
select{
case<-updateTicker.C:
//... doSomething
case<-done:
// cancel the it by close done channel
return
}
}
}()
3.5. How to loop each attribute of struct
For example, we want to convert a struct into []string such that it could be write into CSV files (encoding/csv need data to be []string)
type LoadtestRecord struct {
Id int
Timestamp time.Time
TesterName string
Guid string
Linetype string
OpenPartDuration time.Duration
CompleteDuration time.Duration
Text string
SinceBegin time.Duration
}
func (l LoadtestRecord) toStrings() []string {
v := reflect.ValueOf(l)
typeOfS := v.Type()
result := []string{}
for i := 0; i < v.NumField(); i++ {
k := typeOfS.Field(i).Name
v := v.Field(i).Interface()
if k == "Timestamp" {
result = append(result, formatTime(v.(time.Time)))
} else if k == "OpenPartDuration" {
result = append(result, fmt.Sprintf("%v", int64(v.(time.Duration)/time.Millisecond)))
} else if k == "CompleteDuration" {
result = append(result, fmt.Sprintf("%v", int64(v.(time.Duration)/time.Millisecond)))
} else if k == "Id" || k == "SinceBegin" {
continue
} else {
str := v.(string)
result = append(result, removeLastStr(strings.TrimSpace(str), "\n"))
}
}
return result
}
- Here, we define a method on the type
LoadtestRecordsuch it we could convert subset of its attributes into[]stringto save it into CSV file.
3.6. How to build command-line tools
So easy in Golang
var n, p, t int var minNumOfEC2, maxNumOfEC2 int var stackEnv, workerVersion, jsonFile string // var setUTC bool flag.IntVar(&n, "n", 1, "number of testers to launch per path") flag.IntVar(&p, "p", 1, "how many patch") flag.IntVar(&t, "t", 5, "time interval(seconds) between patch") flag.StringVar(&stackEnv, "e", "dev", "stack stackEnv, for example: dev, preprod...etc") flag.StringVar(&workerVersion, "w", "Worker.3100", "the worker version used for tester, by default it will use Worker.3100") flag.StringVar(&jsonFile, "j", "input_clouddash_dev.json", "the json file contains the APIs used by tester, by default it will use input_clouddash_dev.json") flag.BoolVar(&setUTC, "utc", false, "time displayed in log will be using UTC by default, -utc=true to set timestamp in UTC") flag.Parse()
3.7. How to write unit test
- ref: Structuring Tests in Go
- ref: 5 simple tips and tricks for writing unit tests in #golang
- ref: Interface in Go
- ref: Mocking Out the AWS SDK for Go for Unit Testing
- Summary
- How to structure go test
- Put test code in the same package as target code.
- Naming convention: if target code is
main.go, then test code ismain_test.go
- Put test code in the same package as target code.
- How to use interface to mock dependencies during test.
- Ensure dependencies are passed as parameters of function(dependency injection makes function testable).
- Identify the methods of dependency.
- Create interface in which those dependent methods are defined.
- Re-define function parameter as interface instead of concrate type.
- In test code
- Define Mocking type
- Make Mocking type implement the interface (those dependent methods).
- The caller declares the Mocking type and pass it into target testing function.
- Define Mocking type
- Ensure dependencies are passed as parameters of function(dependency injection makes function testable).
- What is the pattern when mock AWS SDK in go (in a function it calls methods from a Go SDK’s Client)
Make testing function injectable: two approaches
- One is pass Client directly as parameter of the function.
- Another is define a struct which contains that Client. Then, define function as method of such struct.
In anyway, we don’t use concrate Client type, we use AWS’s interface type.
- One is pass Client directly as parameter of the function.
- Here, the steps of identify and create our own interface to hold those methods are ignored. Because AWS already defines those interfaces, we just need to import them and use.
- In test code
- Define Mocking type
- Make Mocking type implement the interface (those dependent methods).
- Define testing cases as slice in which it contains different cases for input and expecting output
- Use
for ... rangeto loop those cases, in each of them:
- Initialize Mocking type with different input
- Pass initialized Mocking type as parameter of testing function or as attribute of struct.
- During the testing, the testing function is got executed which calls those dependent methods
- That is OK, because Mocking type implements those method from interface.
- Initialize Mocking type with different input
- Define Mocking type
- How to structure go test
4. Understand Reader and Writer interface
In Golang, io.Reader and Writer are interfaces, all methods related with I/O are just streams of bytes getting passed around.
4.1. Understand interface
In Golang standard library, a lot of components are interfaces. For example
type Example struct {
}
func (e *Example) Write(p byte[]) (n int, err error) {
}
func (e *Example) Read(p byte[]) (n int, err error) {
}
- A interface will group none, or multiple functions.
- A type implements those functions as methods satisfies that interface.
Exampleimplements Read/Write interface. - If we use type conversion,
Examplecould be type of Reader && Writer.
4.2. Io.Reader
For a type of function as a reader, it must implement method Read(p []byte) from interface io.Reader.
type Reader interface {
Read(p []byte) (n int, err error)
}
Read()will read up tolen(p)intop, when possible.- After a
Read()call,nmay be less thenlen(p). - Upon error,
Read()may still returnnbytes in bufferp. For instance, when reading from a TCP socket that is abruptly closed. You may choose to keep the bytes inpor retry.
4.3. Io.Writer
4.4. Use pipe to switch from Writer to Reader
Sometimes a function’s parameters only accept Writer, but we want a Reader. The solution is io.Pipe().
4.5. An implementation of io.Reader which filters out non-alphabetic characters from its stream.
type alphaReader struct {
src string
cur int
}
func newAlphaReader(src string) *alphaReader {
return &alphaReader{src: src}
}
func alpha(r byte) byte {
if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') {
return r
}
return 0
}
func (a *alphaReader) Read(p []byte) (int, error) {
if a.cur >= len(a.src) {
return 0, io.EOF
}
x := len(a.src) - a.cur
n, bound := 0, 0
if x >= len(p) {
bound = len(p)
} else if x <= len(p) {
bound = x
}
buf := make([]byte, bound)
for n < bound {
if char := alpha(a.src[a.cur]); char != 0 {
buf[n] = char
}
n++
a.cur++
}
copy(p, buf)
return n, nil
}
func main() {
reader := newAlphaReader("Hello! It's 9am, where is the sun?")
p := make([]byte, 4)
for {
n, err := reader.Read(p)
if err == io.EOF {
break
}
fmt.Print(string(p[:n]))
}
fmt.Println()
}
Notice the display of
[]byteasstringin different terminal shows different result:
fmt.Println(string([]byte{111, 73})) // on terminal, it is displayed as oI fmt.Println(string([]byte{111, 0, 0, 0, 0, 73})) // on terminal it is displayed as oI, but for some other terminal, it is displayed as o I
4.6. References
- Demystifying Golang’s io.Reader and io.Writer Interfaces (about using pipe to switch writer for reader)
- Streaming IO in Go ()
- The Beauty of io.Writer
- Talk about the confusion about Golang IO reading and writing
5. Build code into executable binary file
While the go run command is a useful shortcut for compiling and running a program when you’re making frequent changes, it doesn’t generate a binary executable. (See: Compile and install the application)
- ref: How To Build Go Executables for Multiple Platforms on Ubuntu 16.04
create script
#!/usr/bin/env bash package=$1 if [[ -z "$package" ]]; then echo "usage: $0 <package-name>" exit 1 fi package_split=(${package//\// }) package_name=${package_split[-1]} platforms=("windows/amd64" "windows/386" "linux/amd64") for platform in "${platforms[@]}" do platform_split=(${platform//\// }) GOOS=${platform_split[0]} GOARCH=${platform_split[1]} output_name=$package_name'-'$GOOS'-'$GOARCH if [ $GOOS = "windows" ]; then output_name+='.exe' fi env GOOS=$GOOS GOARCH=$GOARCH go build -o $output_name $package if [ $? -ne 0 ]; then echo 'An error has occurred! Aborting the script execution...' exit 1 fi done
Use the above script to build out loadtest file is just
./go-executable-build.sh main/mLoadTest.go
It will generate the following files for the platforms we defined
mLoadTest.go-linux-amd64 mLoadTest.go-windows-386.exe mLoadTest.go-windows-amd64.exe
6. Concurrency in Go
I choose go to do this loadtest job because of this. There are several common patterns.
6.1. How to receive signal from event, such as Ctr-C
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
done := make(chan interface{})
go func() {
select {
case <-time.After(globalTimeoutLimit):
fmt.Println("timeout globablly, cancel all")
close(done)
case <-interrupt:
// use interrupt to gracefully cancel all tester jobs
fmt.Println("interrupt, cancel all")
close(done)
}
}()
6.2. From master spawn workers and cancel them when needed
func observingEC2Instances(done <-chan interface{}, stackEnv string, minNumOfEC2, maxNumOfEC2 *int) {
// observing the changes of EC2
observeEC2Ticker := time.NewTicker(10 * time.Second)
fname := filepath.Join(logFolder, fmt.Sprintf("number-of-EC2-instance.txt"))
f, _ := os.Create(fname)
defer observeEC2Ticker.Stop()
defer f.Close()
fmt.Println("Observing number of EC2 instances")
num := aws.GetNumOfEC2(fmt.Sprintf("ecs-%v", stackEnv))
f.Write([]byte(fmt.Sprintf("NumEC2s\tTime\n")))
tStr := formatTime(time.Now())
f.Write([]byte(fmt.Sprintf("%v\t%v\n", num, tStr)))
*minNumOfEC2 = num
*maxNumOfEC2 = num
for {
select {
case <-done:
return
case <-observeEC2Ticker.C:
num = aws.GetNumOfEC2(fmt.Sprintf("ecs-%v", stackEnv))
tStr = formatTime(time.Now())
f.Write([]byte(fmt.Sprintf("%v\t%v\n", num, tStr)))
if num > *maxNumOfEC2 {
*maxNumOfEC2 = num
}
if num < *minNumOfEC2 {
*minNumOfEC2 = num
}
}
}
}
- We usually pass
done := make(chan interface{})into each worker’s goroutine. - Then in it, use
for...selectpattern to do cleanup operation once we receive messages fromdone. - Here, we could observe the number of EC2 instance with time interval and cancel it if we want in a goroutine.
7. Summary
- The biggest thought about Golang is its standard library can do so many things. A lot of them need third party library in Javascript.
- Its concurrency pattern is great, for a structure of tree shape concurrency, it is managable. But it soon becomes complicated if we want to form a net of goroutines. Erlang/Elixir is much better to compose complicated newtork of concurrency, especially if we want execute them across on different host.