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/bin
is 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/tour
To 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.go
to see if it works. We will implement different packages(
.go
and*_test.go
in one folder) under thisloadtest
module(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/http
module 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.Unmarshal
it. And it will fill our struct autmatically based some rules (seeencoding/json
for 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
LoadtestRecord
such it we could convert subset of its attributes into[]string
to 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 ... range
to 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.
Example
implements Read/Write interface. - If we use type conversion,
Example
could 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,n
may be less thenlen(p)
. - Upon error,
Read()
may still returnn
bytes in bufferp
. For instance, when reading from a TCP socket that is abruptly closed. You may choose to keep the bytes inp
or 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
[]byte
asstring
in 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...select
pattern 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.