Golang Unit Test
使用 Go 進行單元測試
package main
import "testing"
func TestHello(t *testing.T) {
got := Hello()
want := "Hello, world"
if got != want {
t.Errorf("got '%s' want '%s'", got, want)
}
}
package main
import "fmt"
func Hello() string {
return "Hello, world"
}
func main() {
fmt.Println(Hello())
}
go test
PASS
ok .../Golang_TDD 0.086s
Array
TDD 模式 先寫測試在寫code
// 測試數字相加
func TestSumAll(t *testing.T) {
got := SumAll([]int{1, 2}, []int{0, 9})
want := []int{3, 9}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v want %v", got, want)
}
}
// 測試去除第一數陣列後相加
func TestSumAllTails(t *testing.T) {
got := SumAllTails([]int{1,2}, []int{0,9})
want := []int{2, 9}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v want %v", got, want)
}
}
func Sum(numbers []int) int {
sum := 0
for _, number := range numbers {
sum += number
}
return sum
}
func SumAllTails(numbersToSum ...[]int) []int {
var sums []int
for _, numbers := range numbersToSum {
tail := numbers[1:]
sums = append(sums, Sum(tail))
}
return sums
}
製造錯誤
// 使用空陣列讓原有程式造成 panic
t.Run("safely sum empty slices", func(t *testing.T) {
got := SumAllTails([]int{}, []int{3, 4, 5})
want := []int{0, 9}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v want %v", got, want)
}
})
// error logs
panic: runtime error: slice bounds out of range [recovered]
panic: runtime error: slice bounds out of range
修正錯誤
func SumAllTails(numbersToSum ...[]int) []int {
var sums []int
for _, numbers := range numbersToSum {
// 增加判斷numbers 為空判斷
if len(numbers) == 0 {
sums = append(sums, 0)
} else {
tail := numbers[1:]
sums = append(sums, Sum(tail))
}
}
return sums
}
減少 test 重複 code
checkSums := func(t *testing.T, got, want []int) {
// 告知 testing 這是一個 helper function 當發生與預期不符時會指向呼叫 checksums的行數
t.Helper()
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v want %v", got, want)
}
}
結構與介面
func TestArea(t *testing.T) {
// shape interface
checkArea := func(t *testing.T, shape Shape, want float64) {
t.Helper()
got := shape.Area()
if got != want {
t.Errorf("got %.2f want %.2f", got, want)
}
}
t.Run("rectangles", func(t *testing.T) {
rectangle := Rectangle{12, 6}
// Rectangle struct have Area member function
checkArea(t, rectangle, 72.0)
})
t.Run("circles", func(t *testing.T) {
circle := Circle{10}
// Circle struct have Area member function
checkArea(t, circle, 314.1592653589793)
})
}
// 介面
type Shape interface {
Area() float64
}
// 結構
type Rectangle struct {
Width float64
Height float64
}
// 結構方法
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
Table Driven Tests
func TableDrivenTestsArea(t *testing.T) {
// 批次測試
areaTests := []struct {
shape Shape
want float64
}{
{Rectangle{12, 6}, 72.0},
{Circle{10}, 314.1592653589793},
}
for _, tt := range areaTests {
got := tt.shape.Area()
if got != tt.want {
t.Errorf("got %.2f want %.2f", got, tt.want)
}
}
}
執行特定測試
func TestTableDrivenArea(t *testing.T) {
areaTests := []struct {
name string
shape Shape
hasArea float64
}{
{name: "Rectangle", shape: Rectangle{Width: 12, Height: 6}, hasArea: 72.0},
{name: "Circle", shape: Circle{Radius: 10}, hasArea: 314.1592653589793},
}
for _, tt := range areaTests {
t.Run(tt.name, func(t *testing.T) {
got := tt.shape.Area()
if got != tt.hasArea {
t.Errorf("%#v got %.2f want %.2f", tt.shape, got, tt.hasArea)
}
})
}
}
go test -run TestTableDrivenArea/Rectangle
Extends繼承
package main
import "fmt"
type Person struct {
Id int
Name string
}
type Tester interface {
Test()
Eat()
}
func (this *Person) Test() {
fmt.Println("\tthis =", &this, "Person.Test")
}
func (this *Person) Eat() {
fmt.Println("\tthis =", &this, "Person.Eat")
}
// Employee從Person繼承,並直接繼承Eat方法,並且將Test方法覆蓋。
type Employee struct {
Person
}
func (this *Employee) Test() {
fmt.Println("\tthis =", &this, "Employee.Test")
this.Person.Test() // 調用父類別方法
}
func main() {
fmt.Println("An Employee instance :")
var nu Employee
nu.Id = 2
nu.Name = "NTom"
nu.Test()
nu.Eat()
fmt.Println()
fmt.Println("A Tester interface to Employee instance :")
var t Tester
t = &nu
t.Test()
t.Eat()
fmt.Println()
fmt.Println("A Tester interface to Person instance :")
t = &nu.Person
t.Test()
t.Eat()
}
Pointer
以下範例以帳號內存取現金示範
使用 TDD 開發需先撰寫測試文件
func assertNoError(t *testing.T, got error) {
t.Helper()
if got != nil {
t.Fatal("got an error but didnt want one")
}
}
func assertBalance(t *testing.T, wallet Wallet, want Bitcoin) {
t.Helper()
got := wallet.Balance()
if got != want {
t.Errorf("got '%s' want '%s'", got, want)
}
}
func assertError(t *testing.T, got error, want error) {
t.Helper()
if got == nil {
t.Fatal("didn't get an error but wanted one")
}
if got != want {
t.Errorf("got '%s', want '%s'", got, want)
}
}
func TestWallet(t *testing.T) {
t.Run("Deposit", func(t *testing.T) {
wallet := Wallet{}
// 使用 & 檢查 Wallet 記憶體位置是否一至
fmt.Printf("address of Wallet in test is %p \n", &wallet)
wallet.Deposit(Bitcoin(10))
assertBalance(t, wallet, Bitcoin(10))
})
t.Run("Withdraw", func(t *testing.T) {
wallet := Wallet{Bitcoin(20)}
err := wallet.Withdraw(Bitcoin(10))
assertBalance(t, wallet, Bitcoin(10))
assertNoError(t, err)
})
t.Run("Withdraw insufficient funds", func(t *testing.T) {
wallet := Wallet{Bitcoin(20)}
err := wallet.Withdraw(Bitcoin(100))
assertBalance(t, wallet, Bitcoin(20))
assertError(t, err, ErrInsufficientFunds)
})
}
// 創建一個 error 發生
var ErrInsufficientFunds = errors.New("cannot withdraw, insufficient funds")
// 設定新 type 並指定為 int
type Bitcoin int
// Bitcoin to string 時回傳 %d BTC 結果
func (b Bitcoin) String() string {
return fmt.Sprintf("%d BTC", b)
}
// 建構 Wallet struct 並擁有型別為 Bitcoin 的成員 balance
type Wallet struct {
balance Bitcoin
}
// 建立成員方法 (w *Wallet) *代表使用記憶體位置取得 Wallet struct
func (w *Wallet) Deposit(amount Bitcoin) {
// w *Wallet 的pointer(*) w 已經代表記憶體位置本身,所已不需 &
fmt.Printf("address of Wallet in Deposit is %p \n", w)
w.balance += amount
}
func (w *Wallet) Withdraw(amount Bitcoin) error {
// 檢查所領款的金額是否超過存款金額
if amount > w.balance {
// 如果超過回傳 error
return ErrInsufficientFunds
}
w.balance -= amount
return nil
}
func (w *Wallet) Balance() Bitcoin {
return w.balance
}
Map
定義一個 map[KeyType]ValueType 的成員
// 建立 map 兩種方法
dictionary = map[string]string{}
// OR
dictionary = make(map[string]string)
撰寫測試文件
func TestSearch(t *testing.T) {
dictionary := map[string]string{"test": "this is just a test"}
got := Search(dictionary, "test")
want := "this is just a test"
if got != want {
t.Errorf("got '%s' want '%s' given, '%s'", got, want, "test")
}
}
根據測試文件撰寫符合規格的程式
func Search(dictionary map[string]string, word string) string {
return dictionary[word]
}
優化重構程式
func TestSearchRefactor(t *testing.T) {
dictionary := Dictionary{"test": "this is just a test"}
// 檢查 Dictionary記憶體位置是否一至,這邊不需用&取得記憶體位置,Map 本身就是 reference types
fmt.Printf("address of Dictionary in test is %p \n", dictionary)
t.Run("known word", func(t *testing.T) {
got, _ := dictionary.Search("test")
want := "this is just a test"
assertStrings(t, got, want)
})
t.Run("unknown word", func(t *testing.T) {
_, err := dictionary.Search("unknown")
want := "could not find the word you were looking for"
if err == nil {
t.Fatal("expected to get an error.")
}
assertStrings(t, err.Error(), want)
})
}
type Dictionary map[string]string
var ErrNotFound = errors.New("could not find the word you were looking for")
// 這邊的 Dictionary 物件成員並沒有使用 * 因為 map 本身就是 reference types 所以不需重新指向記憶體位置
func (d Dictionary) Search(word string) (string, error) {
fmt.Printf("address of Dictionary in Search is %p \n", d)
definition, ok := d[word]
if !ok {
return "", ErrNotFound
}
return definition, nil
}
新增新功能 Add
func TestAdd(t *testing.T) {
dictionary := Dictionary{}
dictionary.Add("test", "this is just a test")
want := "this is just a test"
got, err := dictionary.Search("test")
if err != nil {
t.Fatal("should find added word:", err)
}
if want != got {
t.Errorf("got '%s' want '%s'", got, want)
}
}
根據測試文件開發功能
func (d Dictionary) Add(word, definition string) {
d[word] = definition
}
優化重構程式
func assertDefinition(t *testing.T, dictionary Dictionary, word, definition string) {
t.Helper()
got, err := dictionary.Search(word)
if err != nil {
t.Fatal("should find added word:", err)
}
if definition != got {
t.Errorf("got '%s' want '%s'", got, definition)
}
}
func TestAdd(t *testing.T) {
t.Run("new word", func(t *testing.T) {
dictionary := Dictionary{}
word := "test"
definition := "this is just a test"
err := dictionary.Add(word, definition)
assertError(t, err, nil)
assertDefinition(t, dictionary, word, definition)
})
t.Run("existing word", func(t *testing.T) {
word := "test"
definition := "this is just a test"
dictionary := Dictionary{word: definition}
err := dictionary.Add(word, "new test")
assertError(t, err, ErrWordExists)
assertDefinition(t, dictionary, word, definition)
})
}
const (
ErrNotFound = DictionaryErr("could not find the word you were looking for")
ErrWordExists = DictionaryErr("cannot add word because it already exists")
)
type DictionaryErr string
func (e DictionaryErr) Error() string {
return string(e)
}
func (d Dictionary) Add(word, definition string) error {
_, err := d.Search(word)
switch err {
case ErrNotFound:
d[word] = definition
case nil:
return ErrWordExists
default:
return err
}
return nil
}
新增新功能Update
t.Run("existing word", func(t *testing.T) {
word := "test"
definition := "this is just a test"
newDefinition := "new definition"
dictionary := Dictionary{word: definition}
err := dictionary.Update(word, newDefinition)
assertError(t, err, nil)
assertDefinition(t, dictionary, word, newDefinition)
})
t.Run("new word", func(t *testing.T) {
word := "test"
definition := "this is just a test"
dictionary := Dictionary{}
err := dictionary.Update(word, definition)
assertError(t, err, ErrWordDoesNotExist)
})
const (
ErrNotFound = DictionaryErr("could not find the word you were looking for")
ErrWordExists = DictionaryErr("cannot add word because it already exists")
// new
ErrWordDoesNotExist = DictionaryErr("cannot update word because it does not exist")
)
func (d Dictionary) Update(word, definition string) error {
_, err := d.Search(word)
switch err {
case ErrNotFound:
return ErrWordDoesNotExist
case nil:
d[word] = definition
default:
return err
}
return nil
}
新增新功能Delete
func TestDelete(t *testing.T) {
word := "test"
dictionary := Dictionary{word: "test definition"}
dictionary.Delete(word)
_, err := dictionary.Search(word)
if err != ErrNotFound {
t.Errorf("Expected '%s' to be deleted", word)
}
}
func (d Dictionary) Delete(word string) {
// go lang 內建刪除功能
delete(d, word)
}
Dependency Injection
使用依賴性注入將會有已下優點
- 不需要一個特定框架
- 不會將你的code變得更加複雜
- 更有助於程式測試
- 您將會編寫出色的通用功能方法
讓我們回到最初的hello方法
func TestGreet(t *testing.T) {
buffer := bytes.Buffer{}
Greet(&buffer,"Chris")
got := buffer.String()
want := "Hello, Chris"
if got != want {
t.Errorf("got '%s' want '%s'", got, want)
}
}
func Greet(writer *bytes.Buffer, name string) {
// fmt.Fprintf 就像是 fmt.Printf 一樣,但他用 writer 取代了預設的 stdout
fmt.Fprintf(writer, "Hello, %s", name)
}
讓我們看看 go 的 fmt 實現
// It returns the number of bytes written and any write error encountered.
func Printf(format string, a ...interface{}) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
p := newPrinter()
p.doPrintf(format, a)
n, err = w.Write(p.buf)
p.free()
return
}
type Writer interface {
Write(p []byte) (n int, err error)
}
我們嘗試使用 Greet function
// 失敗因為 os.Stdout 不是 *bytes.Buffer type
Greet(os.Stdout, "Elodie")
修正錯誤
// 我們使用 io.Writer 來解決寫入問題
func Greet(writer io.Writer, name string) {
fmt.Fprintf(writer, "Hello, %s", name)
}
現在已改寫完成了 Greet 方法,我們來嘗試用他來輸出至網頁上
package main
import (
"fmt"
"io"
"net/http"
)
func Greet(writer io.Writer, name string) {
fmt.Fprintf(writer, "Hello, %s", name)
}
func MyGreeterHandler(w http.ResponseWriter, r *http.Request) {
Greet(w, "world")
}
func main() {
http.ListenAndServe(":5000", http.HandlerFunc(MyGreeterHandler))
}
Mocking
我們開發了一個程式,必須到數3秒代碼如下:
func TestCountdown(t *testing.T) {
buffer := &bytes.Buffer{}
Countdown(buffer)
got := buffer.String()
want := `3
2
1
Go!`
if got != want {
t.Errorf("got '%s' want '%s'", got, want)
}
}
const finalWord = "Go!"
const countdownStart = 3
func Countdown(out io.Writer) {
for i := countdownStart; i > 0; i-- {
time.Sleep(1 * time.Second)
fmt.Fprintln(out, i)
}
time.Sleep(1 * time.Second)
fmt.Fprint(out, finalWord)
}
不難發現當執行測試時,必須花費超過3秒的時間,想像當我們程式更加複雜後,必須要花多久時間執行測試呢?
所以以下我們改變了寫法 :
func TestCountdownMock(t *testing.T) {
buffer := &bytes.Buffer{}
// 我們創建了一個假的 sleep 方法
spySleeper := &SpySleeper{}
CountdownMock(buffer, spySleeper)
got := buffer.String()
want := `3
2
1
Go!`
if got != want {
t.Errorf("got '%s' want '%s'", got, want)
}
if spySleeper.Calls != 4 {
t.Errorf("not enough calls to sleeper, want 4 got %d", spySleeper.Calls)
}
}
type Sleeper interface {
Sleep()
}
type SpySleeper struct {
Calls int
}
func (s *SpySleeper) Sleep() {
s.Calls++
}
type DefaultSleeper struct {}
func (d *DefaultSleeper) Sleep() {
time.Sleep(1 * time.Second)
}
func CountdownMock(out io.Writer, sleeper Sleeper) {
for i := countdownStart; i > 0; i-- {
sleeper.Sleep()
fmt.Fprintln(out, i)
}
sleeper.Sleep()
fmt.Fprint(out, finalWord)
}
func main() {
// 真實 sleep
sleeper := &DefaultSleeper{}
Countdown(os.Stdout, sleeper)
}
通過以上代碼我們測試時明顯減少了等待時間,但是仍然有些問題我們還尚未測試,他是否是依照 先執行 sleep 後再執行 print 直到最後。
const write = "write"
const sleep = "sleep"
type CountdownOperationsSpy struct {
// 字串陣列,記錄呼叫的方法
Calls []string
}
// 記錄Sleep
func (s *CountdownOperationsSpy) Sleep() {
s.Calls = append(s.Calls, sleep)
}
// 記錄Write
func (s *CountdownOperationsSpy) Write(p []byte) (n int, err error) {
s.Calls = append(s.Calls, write)
return
}
// 監控 Time 時間
type SpyTime struct {
durationSlept time.Duration
}
// 記錄下睡眠時間
func (s *SpyTime) Sleep(duration time.Duration) {
s.durationSlept = duration
}
func TestCountdown(t *testing.T) {
// 測試輸出資料是否正確
t.Run("prints 5 to Go!", func(t *testing.T) {
buffer := &bytes.Buffer{}
Countdown(buffer, &CountdownOperationsSpy{})
got := buffer.String()
want := `3
2
1
Go!`
if got != want {
t.Errorf("got '%s' want '%s'", got, want)
}
})
// 測試執行 function 順序是否正確
t.Run("sleep after every print", func(t *testing.T) {
spySleepPrinter := &CountdownOperationsSpy{}
Countdown(spySleepPrinter, spySleepPrinter)
want := []string{
sleep,
write,
sleep,
write,
sleep,
write,
sleep,
write,
}
if !reflect.DeepEqual(want, spySleepPrinter.Calls) {
t.Errorf("wanted calls %v got %v", want, spySleepPrinter.Calls)
}
})
}
func TestConfigurableSleeper(t *testing.T) {
// 測試 ConfigurableSleeper 是否執行正確的睡眠時間
sleepTime := 5 * time.Second
spyTime := &SpyTime{}
sleeper := ConfigurableSleeper{sleepTime, spyTime.Sleep}
sleeper.Sleep()
if spyTime.durationSlept != sleepTime {
t.Errorf("should have slept for %v but slept for %v", sleepTime, spyTime.durationSlept)
}
}
// Sleeper 介面
type Sleeper interface {
Sleep()
}
// ConfigurableSleeper 實做 Sleeper 且定義 delay 時間
type ConfigurableSleeper struct {
duration time.Duration
sleep func(time.Duration)
}
// Sleep將會依照 duration 時間暫停程式執行
func (c *ConfigurableSleeper) Sleep() {
c.sleep(c.duration)
}
const finalWord = "Go!"
const countdownStart = 3
// Countdown 將根據 delay 時間印出倒數
func Countdown(out io.Writer, sleeper Sleeper) {
for i := countdownStart; i > 0; i-- {
sleeper.Sleep()
fmt.Fprintln(out, i)
}
sleeper.Sleep()
fmt.Fprint(out, finalWord)
}
func main() {
sleeper := &ConfigurableSleeper{1 * time.Second, time.Sleep}
Countdown(os.Stdout, sleeper)
}
Concurrency
我們以測試網站是否正常為例
func mockWebsiteChecker(url string) bool {
if url == "waat://furhurterwe.geds" {
return false
}
return true
}
func TestCheckWebsites(t *testing.T) {
websites := []string{
"http://google.com",
"http://blog.gypsydave5.com",
"waat://furhurterwe.geds",
}
want := map[string]bool{
"http://google.com": true,
"http://blog.gypsydave5.com": true,
"waat://furhurterwe.geds": false,
}
got := CheckWebsites(mockWebsiteChecker, websites)
if !reflect.DeepEqual(want, got) {
t.Fatalf("Wanted %v, got %v", want, got)
}
}
func slowStubWebsiteChecker(_ string) bool {
time.Sleep(20 * time.Millisecond)
return true
}
func BenchmarkCheckWebsites(b *testing.B) {
urls := make([]string, 100)
for i := 0; i < len(urls); i++ {
urls[i] = "a url"
}
for i := 0; i < b.N; i++ {
CheckWebsites(slowStubWebsiteChecker, urls)
}
}
type WebsiteChecker func(string) bool
type result struct {
string
bool
}
func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool {
results := make(map[string]bool)
resultChannel := make(chan result)
for _, url := range urls {
go func(u string) {
resultChannel <- result{u, wc(u)}
}(url)
}
for i := 0; i < len(urls); i++ {
result := <-resultChannel
results[result.string] = result.bool
}
return results
}
Select
func TestRacer(t *testing.T) {
t.Run("compares speeds of servers, returning the url of the fastest one", func(t *testing.T) {
slowServer := makeDelayedServer(20 * time.Millisecond)
fastServer := makeDelayedServer(0 * time.Millisecond)
// defer 延遲動作,用於當需要執行完function後close物件
defer slowServer.Close()
defer fastServer.Close()
slowURL := slowServer.URL
fastURL := fastServer.URL
want := fastURL
got, err := Racer(slowURL, fastURL)
if err != nil {
t.Fatalf("did not expect an error but got one %v", err)
}
if got != want {
t.Errorf("got '%s', want '%s'", got, want)
}
})
t.Run("returns an error if a server doesn't respond within 10s", func(t *testing.T) {
server := makeDelayedServer(25 * time.Millisecond)
defer server.Close()
_, err := ConfigurableRacer(server.URL, server.URL, 20*time.Millisecond)
if err == nil {
t.Error("expected an error but didn't get one")
}
})
}
func makeDelayedServer(delay time.Duration) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(delay)
w.WriteHeader(http.StatusOK)
}))
}
var tenSecondTimeout = 10 * time.Second
// 測試 a b 網站速度,超過 10s timeout
func Racer(a, b string) (winner string, error error) {
return ConfigurableRacer(a, b, tenSecondTimeout)
}
// 比較 a b 網站 並回傳速度較快的一個
func ConfigurableRacer(a, b string, timeout time.Duration) (winner string, error error) {
// 使用 select 同時並發動作
select {
case <-ping(a):
return a, nil
case <-ping(b):
return b, nil
case <-time.After(timeout):
return "", fmt.Errorf("timed out waiting for %s and %s", a, b)
}
}
func ping(url string) chan bool {
ch := make(chan bool)
go func() {
http.Get(url)
ch <- true
}()
return ch
}
Pointer Panic
type Calc interface {
increment()
decrement()
}
type Math struct {
count int
}
func (m *Math) increment() {
println(m)
m.count += 1
println("increment!", m.count)
}
func (m *Math) decrement() {
println(&m)
m.count -= 1
println("decrement!", m.count)
}
func (m Math) decrement2() {
m.count -= 1
println("decrement!", m.count)
}
func main() {
// explodes -> math -> nil
var math *math = nil
(*math).increment() // panic: value method main.math.decrement called using nil *math pointer
var explodes Explodes = math
println(math, explodes) // '0x0 (0x10a7060,0x0)'
if explodes != nil {
println("Not nil!") // 'Not nil!'
explodes.increment() // Normal
explodes.decrement2() // panic: value method main.math.decrement called using nil *math pointer
} else {
println("nil!")
}
var math1 math = math{}
println(&math1)
math1.increment()
println(&math1)
math1.increment()
println(&math1)
math1.increment()
println(math1.count)
var math2 *math = &math{}
println(&math2)
math2.increment()
println(&math2)
math2.increment()
println(&math2)
math2.increment()
println(math2.count)
}
對於 golang 並無try catch 的解釋
https://www.ithome.com.tw/voice/103455
Panic Recover
package main
import (
"fmt"
"os"
)
func check(err error) {
if err != nil {
panic(err)
}
}
func main() {
f, err := os.Open("/tmp/dat")
defer func() {
if err := recover(); err != nil {
fmt.Println(err) // 這已經是頂層的 UI 介面了,想以自己的方式呈現錯誤
}
if f != nil {
if err := f.Close(); err != nil {
panic(err) // 示範再拋出 panic
}
}
}()
check(err)
b1 := make([]byte, 5)
n1, err := f.Read(b1)
check(err)
fmt.Printf("%d bytes: %s\n", n1, string(b1))
}
Summary
優點 |
---|
使用 goroutines、channels 快速開發並型程序 |
Go 性能明顯優越於近代語言(除了 Rust) |
標準內建的測試框架 |
有 Defer功能不會忘記關閉清除物件 |
缺點 |
---|
沒有泛型 |
沒有 enum |
數據結構只有 map,slice |
擁有指標特性可能使開發者混亂 |
Reference
TDD 開發模式
CI/CD 使用
https://github.com/drone/drone
ORM Library 測試
https://github.com/mattn/go-oci8/tree/master/_example
https://www.jianshu.com/p/add47894c446
https://gocodecloud.com/blog/2016/08/09/accessing-an-oracle-db-in-go/
https://blog.csdn.net/notbaron/article/details/78168026
https://o7planning.org/en/10467/installing-c-cpp-compiler-mingw
https://github.com/wendal/go-oci8
https://www.oracle.com/technetwork/topics/winx64soft-089540.html
https://www.144d.com/post-490.html
https://www.cnblogs.com/ghj1976/p/3540257.html
https://medium.com/@kfoss/oracle-and-go-on-windows-64-bit-2b37496fe179
gRPC 使用
https://grpc.io/docs/quickstart/go.html#prerequisites
MicroServices 架構開發
https://yq.aliyun.com/articles/2764
https://lostechies.com/andrewsiemer/2016/01/11/testing-microservices/
https://blog.csdn.net/tengxing007/article/details/78289645
https://juejin.im/post/5a3257496fb9a044fc44c3d3
https://medium.com/seek-blog/microservices-in-go-2fc1570f6800
https://outcrawl.com/go-microservices-cqrs-docker
https://dzone.com/articles/go-microservices-blog-series-part-1
https://en.wikipedia.org/wiki/Software_architecture
Go library
https://github.com/avelino/awesome-go
Top comments (0)