testify: Testing in Go

假設你把 project 放在 $GOPATH/src/github.com/your_username/ 底下

test

in xxx_test.go

package pangu_test

import (
    "github.com/stretchr/testify/suite"
    "github.com/your_username/your_project"
    "testing"
)

type YourTestSuite struct {
    suite.Suite
}

func (suite *YourTestSuite) TestFunction1() {
    suite.Equal("expected", "actual")
}

// In order for 'go test' to run this suite, we need to create
// a normal test function and pass our suite to suite.Run
func TestYourTestSuite(t *testing.T) {
    suite.Run(t, new(YourTestSuite))
}

go test 會自動執行當前目錄下的所有以 _test.go 結尾的檔案
更準確地說是執行所有以 Test 開頭的 functions

# run tests in the current directory
$ go test

# run tests in the current directory and sub-directories
$ go test ./...

ref:
http://golang.org/pkg/testing/
https://github.com/stretchr/testify

How to specify test resources?
https://groups.google.com/forum/#!topic/golang-nuts/VPVlIiO5yXw

example

in example_test.go

這個檔名是固定的
裡面的內容會出現在 godoc 的 Example 條目底下
確切地說
ExampleFunction1() 這個 function 的內容就會出現在 Function1() 的 Example 條目中
你可以執行 godoc -http=:6060 在本機查看

因為也是以 _test.go 結尾的關係
所以執行 go test 的時候也會執行這個檔案
貼心地幫你確認你的 example 程式碼是不是真的能夠動

package pangu_test

import (
    "fmt"
    "github.com/your_username/your_project"
)

func ExampleFunction1() {
    fmt.Println(`The knights who say "Ni"!`)
    // Output:
    // The knights who say "Ni"!
}

benchmark

in benchmark_test.go

這個檔名是固定的

package pangu_test

import (
    "testing"
)

func BenchmarkFunction1(b *testing.B) {
    for i := 0; i < b.N; i++ {
        doShit()
    }
}
# This will run tests and benchmarks
$ go test -bench=.

# If you want to skip the tests, you can do so by passing a regex to the -run flag that will not match anything. 
$ go test -run=XXX -bench=.

coverage

$ go test -cover -coverprofile=cover.out -covermode=count
$ go tool cover -func=cover.out
$ go tool cover -html=cover.out

# or

$ go test -cover -covermode=count ./...

Integrate with Travis CI and Coveralls

language: go

go:
  - 1.4.2
  - 1.4.1
  - 1.4
  - 1.3.3
  - 1.3.2
  - 1.3.1
  - 1.3
before_install:
  - go get github.com/axw/gocov/gocov
  - go get github.com/mattn/goveralls
  - go get golang.org/x/tools/cmd/cover
script:
  - goveralls -service=travis-ci

ref:
http://docs.travis-ci.com/user/languages/go/
https://coveralls.zendesk.com/hc/en-us/articles/201342809-Go

Go commands

go build

$ go build hello.go
$ ./hello

# 會編譯當前目錄下的所有 .go
# 但是只有 package 是 main 而且擁有 main() 的那個檔案會被編譯成執行檔
# 執行檔的檔名會是目錄名稱而不是 .go 的檔名
$ go build

go build 一個非 package main 的 .go 檔案不會產生任何東西
要執行 go install 才會產生 .a 並安裝到 $GOPATH/pkg

go run

$ go run hello.go

# equals to

$ go build hello.go
$ ./hello

go get

# download and install packages and dependencies
# 會把原始碼存到 `$GOPATH/src`,執行檔放到 `$GOPATH/bin`
$ go get -v github.com/nsf/gocode

# the -u flag instructs get to use the network to update the named packages and their dependencies
$ go get -u github.com/astaxie/bat

# 可以直接安裝某個 repo 的某個子目錄
$ go get github.com/rlmcpherson/s3gof3r/gof3r

# 如果你的 repo 已經在電腦裡了,你可以執行以下指令安裝 dependencies
# the -t flag instructs get to also download the packages required to build the tests
$ go get -t ./...

go install

# 假設 hello_bin 目錄中有一個同名的 hello_bin.go 而它的 package 是 main
# 則 go install 就會編譯 hello_bin.go 為執行檔 hello_bin 然後放到 $GOPATH/bin
$ cd "$GOPATH/src/github.com/vinta/hello_bin"
$ go install

# 假設 hello_lib 目錄中有一個同名的 hello_lib.go 而它的 package 是 hello_lib
# 則 go install 就會編譯 hello_lib.go 為 hello_lib.a 然後放到 $GOPATH/pkg
# 然後你就可以在程式中 import "github.com/vinta/hello_lib"
$ cd "$GOPATH/src/github.com/vinta/hello_lib"
$ go install

go get YOUR_PATH 的 YOUR_PATH 是相對於 $GOPATH/src 的路徑

ref:
http://golang.org/doc/code.html
https://github.com/golang/go/wiki/PackagePublishing

go list

# list all installed packages
$ cd "$GOPATH"
$ go list ./...

go test

# 會編譯並執行目錄下的所有 `*_test.go` 檔案
$ go test
Install Go (Golang)

Install Go (Golang)

Go is a programming language created by Google.

ref:
https://golang.org/

Installation

on Ubuntu:

$ sudo apt-get install -V golang

# or

$ wget https://dl.google.com/go/go1.10.1.linux-amd64.tar.gz
$ tar -C /usr/local -xzf go1.10.1.linux-amd64.tar.gz
$ vim /etc/profile
export PATH=$PATH:/usr/local/go/bin

$ which go

on macOS:

$ brew install go

$ go env
GOCACHE="/Users/vinta/Library/Caches/go-build"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH="/Users/vinta/go"
GOROOT="/usr/local/Cellar/go/1.10/libexec"
GOTOOLDIR="/usr/local/Cellar/go/1.10/libexec/pkg/tool/darwin_amd64"
...

ref:
https://golang.org/dl/

Environment Variables

  • GOPATH is your workspace.
    • go get fetches source code and puts them into $GOPATH/src.

ref:
https://golang.org/doc/code.html

Build a news feed in Redis (fan-out-on-write aka Push model)

但是好像用 Cassandra 會比較適合?

--

可以用 sorted set 來儲存每個用戶的 news feed
用作品的 created_at timestamp 當 score
保持每個 set 的容量為 800 之類的

當用戶發佈了作品之後

就透過 message queue 把這個作品的 id 寫入該用戶的所有 followers 的 news feed 裡
或許也可以只寫入到所謂的 "active users"(例如七天內登入過的用戶)的 news feed 裡
因為明星級的用戶可能有很大量的 followers

news_feed:user_183 = [
user_8:song:5232, 8783421
user_2:song:432, 8509823
user_4:photo:23, 8323490
user_7:album:1232, 5323453
...
]

用 message queue 把作品 id 寫入各用戶的 news feed 時
或許也可以區分活躍用戶與非活躍用戶
活躍用戶的 task 的優先權比較高

當 A 用戶 follow 了 B 用戶之後

可以先取出 A 用戶的 news feed 的最後一個項目
看它的 created_at 是多少
然後撈出 B 用戶的比這個 created_at 還新的作品
接著寫入 A 用戶的 news feed
最後在 truncate 一下(用 ZREMRANGEBYRANK 指令)只保留 800 個項目

當用戶刪除了作品之後

可能有幾種做法:

一是在刪除之後就透過 message queue 遍歷所有 followers 的 news feed
然後把該作品刪掉(用 ZREM 指令)
但是仍然可能遇到還沒刪完就有用戶去讀自己的 news feed 的情況

二是在用戶讀自己的 news feed 的時候才檢查
假設是用 redis 的 list 或 sorted set
一開始就直接讀出 list 裡的 id 列表(當然會分頁)
然後根據 id 再去 mysql 裡撈出真正的 model(例如歌曲、專輯、照片等)
然後檢查有沒有哪些項目被標示為刪除了(假設是虛刪)
有的話就從 redis list 中刪掉
然後再撈下一個分頁的資料來補足
如果還是有應該被刪掉的資料就再重複整個過程

三是幫每個作品維護一個「哪些用戶的 news feed 會有這個作品」的 list
http://stackoverflow.com/questions/12357770/how-to-implement-user-feed-like-in-twitter-or-facebook-on-redis

當然這些方法一起用可能會更好

這裡的刪除可能不只是作品本身被刪除
也可能是 A 用戶 unfollow 了 B 用戶
所以也要把 B 用戶的作品從 A 用戶的 news feed 中刪掉
這個情況的話
似乎只能遍歷 A 用戶的 news feed 來刪掉 B 用戶的作品了?

ref:
http://highscalability.com/blog/2013/10/28/design-decisions-for-scaling-your-high-traffic-feeds.html