BACK
Featured image of post Golang 的超級初心者筆記

Golang 的超級初心者筆記

Go 語言是由 Google 開發的開放原始碼項目,目的之一為了提高開發人員的程式設計效率。 Go 語言語法靈活、簡潔、清晰、高效。它對的並發特性可以方便地用於多核處理器 和網絡開發,同時靈活新穎的類型系統可以方便地撰寫模組化的系統。Go 可以快速編譯, 同時具有記憶體垃圾自動回收功能,並且還支持運行時反射。Go 是一個高效、靜態類型, 但是又具有解釋語言的動態類型特徵的系統級語法。

參考網站
參考網站
參考網站
參考網站


簡介

Go 語言是由 Google 開發的開放原始碼項目,目的之一為了提高開發人員的程式設計效率。
Go 語言語法靈活、簡潔、清晰、高效。它對併發的特性可以方便地用於多核處理器 和網絡開發,同時靈活新穎的類型系統可以方便地撰寫模組化的系統。
Go 可以快速編譯,同時具有記憶體垃圾自動回收功能,並且還支持運行時反射。
Go 是一個高效、靜態類型,但是又具有解釋語言的動態類型特徵的系統級語法。

由來!?

下載中斷是再正常不過的事,但對知名 Google 程式設計師菲茲派翠克(Brad Fitzpatrick)來說,這件事情太難以忍受。

令人不耐的下載速度

其實他並不孤單,多年來很多人都曾抱怨 dl.google.com 系統不夠穩定。

這個系統非常重要,任務範圍幾乎涵蓋所有 Google 下載任務,包括 Chrome 瀏覽器安裝、Android 原始程式碼,以及較小的 JavaScript。但,問題是該檔案伺服器系統的基礎代碼已經超過五年,相當老舊。

菲茲派翠克認為代碼必須不斷更新提升,最初的 C++ 語言缺乏規範的文檔,自動化測試也未達水準,沒有人能搞懂它,只是不斷做一些小幅改變。最後在負責維護的程式人員眼裡,這些代碼根本是一團糟。

不穩定的系統讓伺服器運營團隊相當苦惱,但沒有人有時間重寫代碼。於是菲茲派翠克自告奮勇接手,他在 Google 底下的 25 人工程師團隊 —– 地鼠隊(Gopher Team)中,負責開發一種程式設計語言命名為 Go。這個機會讓他非常興奮。(以上內容節錄自 Wired)

為什麼要學習 Golang?

  • Golang 易學易用:Golang 基本上是強化版的 C 語言,都以核心語法短小精要著稱。
  • Golang 是靜態型別語言:很多程式的錯誤在編譯期就會挑出來,相對易於除錯。
  • Golang 編譯速度很快:帶動整個開發的流程更快速。
  • Golang 支援垃圾回收:網頁程式較接近應用程式,而非系統程式,垃圾回收在這個情境下不算缺點;此外,使用垃圾回收可簡化程式碼。
  • Golang 內建共時性的語法:goroutine 比起傳統的執行緒 (thread) 來說輕量得多,在高負載時所需開銷更少。
  • Golang 是跨平台的:只要程式中不碰到 C 函式庫,在 Windows (或 Mac) 寫好的 Golang 網頁程式,可以不經修改就直接發布在 GNU/Linux 伺服器上。
  • Golang 的專案不需額外的設定檔:在專案中,只要放 Golang 程式碼和一些 assets 即可運作,所需的工具皆內建在 Golang 主程式中,省去學習專案設罝的功夫。
  • Golang 沒有死硬的程式架構:用 Golang 寫網頁程式思維上接近微框架 (micro-framework),只要少數樣板程式碼就可以寫出網頁程式,也不限定可用的第三方函式庫。
  • Golang 函式庫很多,甚至可以直接使用 GitHub 上面的函式庫。
  • Golang 多傳回值,你函式的回傳值可以是多個。
  • 真要說的話,真的列舉不完,以上幾點是我特別看中喜愛的點,希望能勾起大家對於 Golang 的興趣!

但 Golang 並非完美無缺,以下是要考量的點:

  • Golang 並非完整的物件導向 (object-oriented) 語言,頂多是基於物件的 (object-based) 語言。
  • Golang 的語言特性相對少:這是 Golang 時常被攻擊的點,這只能靠自己調整寫程式的習慣。
  • 在一些情境下,Golang 程式碼相對笨拙冗餘,像是排序 (sorting)。

開發環境設定

Go 本身支援 Cross Compile 當然有準備各個平台的安裝方法囉!每次發佈的版本都會包括知名的三大平台 (Windows, Linux, Mac OS),立刻就去下載安裝!

檔案下載網址

等等!Unix Like 的使用者先等等,有個更好用的東西可以讓各位使用,我們有 GVM 可以用,GVM 可以有效的管理 Go 的版本,連 GOPATH 都幫你設定好,超方便的!即時是最新的也都能馬上使用他下載安裝,真的是棒的沒話說,就讓我們來使用他吧!記得要先看一下 Requirements 喔,不然會裝不起來。

另外我的部落格有輕鬆搭建 Go 開發環境的教學 - 在 Windows 平台打造完美的 Go 開發環境 (WSL 2),可以直接參考該篇文章進行搭建!(開發工具使用 Visual Studio Code)


Go 指令

在終端機上執行 go 指令就會看到一系列的指令介紹:

 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
$ go
Go is a tool for managing Go source code.

Usage:

	go command [arguments]

The commands are:

	bug         start a bug report
  build       compile packages and dependencies
  clean       remove object files and cached files
  doc         show documentation for package or symbol
  env         print Go environment information
  fix         update packages to use new APIs
  fmt         gofmt (reformat) package sources
  generate    generate Go files by processing source
  get         add dependencies to current module and install them
  install     compile and install packages and dependencies
  list        list packages or modules
  mod         module maintenance
  work        workspace maintenance
  run         compile and run Go program
  test        test packages
  tool        run specified go tool
  version     print Go version
  vet         report likely mistakes in packages

Use "go help <command>" for more information about a command.

Additional help topics:

  buildconstraint build constraints
  buildmode       build modes
  c               calling between Go and C
  cache           build and test caching
  environment     environment variables
  filetype        file types
  go.mod          the go.mod file
  gopath          GOPATH environment variable
  gopath-get      legacy GOPATH go get
  goproxy         module proxy protocol
  importpath      import path syntax
  modules         modules, module versions, and more
  module-get      module-aware go get
  module-auth     module authentication using go.sum
  packages        package lists and patterns
  private         configuration for downloading non-public code
  testflag        testing flags
  testfunc        testing functions
  vcs             controlling version control with GOVCS

Use "go help <topic>" for more information about that topic.

如果對於某個指令特別需要幫助可以用 go help [topic] 指令。

這邊特別介紹的有四個指令:go rungo buildgo installgo clean

go run

直接執行 golang code。

1
2
3
go run src/helloWorld/main.go

> Hello World

go build

build,如果沒有錯誤就產生執行檔於當前目錄。

1
go build

build 後產生的檔案即是執行檔。

go install

如果沒有錯誤則產生執行檔於 $GOPATH/bin

1
2
3
4
5
6
go install

# 查看執行檔
ls $GOPATH/bin

> helloWorld

go clean

執行後會將 build 產生的檔案都刪除。(install的不會刪)

1
go clean

Javascript v.s. Golang

由於我是從前端寫到後端,這邊用吃飯工具 Javascript 與 Golang 做個寫法比較。

Hello World

  • Javascript:
1
console.log("hello world");
  • Golang:
1
2
3
4
5
6
7
8
package main
import (
  "fmt"
)

func main() {
  fmt.Println("hello world")
}

Array

  • Javascript:
1
const names = ["hello", "world"];
  • Golang:
1
names := []string { "hello", "world" }

印出後面幾個字的子字串

  • Javascript:
1
2
let game = "hello world";
console.log(game.substr(8, game.length));
  • Golang:
1
2
game := "hello world"
fmt.Println(game[8: ])

流程控制

  • Javascript:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const gender = 'female';

switch (gender) {
  case 'female':
    console.log("you are a girl");
    break;
  case 'male':
    console.log("your are a boy");
    break;
  default:
    console.log("you are a third gender");
}
  • Golang:
1
2
3
4
5
6
7
8
9
gender := "female"
switch gender {
case "female":
  fmt.Println("you are a girl")
case "male":
  fmt.Println("your are a boy")
default:
  fmt.Println("you are a third gender")
}

看來 Go 省略了 break 這關鍵字。

Loop

  • Javascript 有 for loop、while loop、do while loop。

  • Go 只有 for loop 就能模擬上面三個。

  • Golang:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
for i := 0; i < 10; i++ {
  fmt.Println(i)
}

// key value pairs
kvs := map[string]string {
  "name":    "wayne blog",
  "website": "https://wayne-blog.com/",
}

for key, value := range kvs {
  fmt.Println(key, value)
}

Object

  • Javascript:
1
2
3
4
5
6
const Post = {
  ID: 10213107,
  Title: "Golang 的新手教學",
  Author: "Wayne",
  Difficulty: "Beginner"
}
  • Golang:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
type Post struct {
  ID int
  Title string
  Author string
  Difficulty string
}

p := Post {
  ID: 10213107,
  Title : "Golang 的新手教學",
  Author: "Wayne",
  Difficulty:"Beginner",
}

Go 能透過定義抽象的 struct 與其屬性,再實例化。

也能透過 map[string]interface 來定義:

1
2
3
4
5
6
Post := map[string]interface{} {
  "ID": 10213107,
  "Title" : "Golang 的新手教學",
  "Author": "Wayne",
  "Difficulty":"Beginner",
}

舉幾個例子比較一下,後面會慢慢補充。


變數型態與宣告方式

Go 和你所知道的大多數語言相同,會有各種型態,如果你有學過 C 語言,這邊你可以很輕鬆的看過去,而且你會發現它跟 C 語言有許多相似之處,Go 之所以被稱為「21 世紀 C 語言不是沒有原因」,另外如果沒有學過,也別擔心,它沒有很困難,那就讓我們來認識看看有哪些型態。

整數

整數的型態很多種,以下表格是 Go 語言擁有的整數型態,以及它的範圍。

類型位元值的範圍
int81-128 ~ 127
uint8(byte)10 ~ 255
int162-32768 ~ 32767
uint1620 ~ 65535
int324-2147483648 ~ 2147483647
uint3240 ~ 4294967295
int648-9223372036854775808 ~ 9223372036854775807
uint6480 ~ 18446744073709551615
int視平台而定視平台而定
uint視平台而定視平台而定
uintptr同指標32 位元平台: 4 位元
64 位元平台: 8 位元

PS:unit(unsigned integer) 意味沒有符號的整數

範例

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
  fmt.Println("1 + 1 =", 1 + 1)
}

浮點數

浮點數囊括任何帶有小數點的數值(.00也算在浮點數)。
浮點數包涵兩種型態 float32、float64。
要注意的是,如果初始化的時候沒有加小數點會被推斷為整數,另外初始化的時候沒有指定型態的話,會被自動推斷為 float64
float64 在 Go 語言相當於 C 語言的 Double,且 float32 與 float64 是無法一起計算的,需要強制轉型

範例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
  var floatValue float64
  floatValue = 7.0
  var floatValue2 = 3.0
  fmt.Println("7.0/3.0 =", floatValue/floatValue2)

  var test float64
  var test2 float32
  test = 1.1
  test2 = 2.2
  fmt.Println("test + test2 =", float32(test) + test2)
}

複數

複數包涵兩種型態 complex64、complex128。
我覺得算是 Go 的特色之一,其他語言很少有這種型態,一個完整的複數需要包涵實數虛數
如果是初始化讓 Go 自動判斷型態的型態會是 complex128,而不會是 complex64。

範例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import "fmt"

func main() {
  var complexValue complex64
  complexValue = 1.2 + 12i
  complexValue2 := 1.2 + 12i
  complexValue3 := complex(3.2, 12)

  fmt.Println("complexValue =", complexValue)
  fmt.Println("complexValue2 =", complexValue2)
  fmt.Println("complexValue3 =", complexValue3)

  fmt.Println("complexValue3 實數 =", real(complexValue3))
  fmt.Println("complexValue3 虛數 =", imag(complexValue3))
}

字串

請注意字串型態無法在初始化後被修改。

範例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import "fmt"

func main() {
  fmt.Println("1" + "1")
  fmt.Println(len("Hello World"))
  fmt.Println("Hello World"[1])
  fmt.Println("Hello" + "World")

  a := "Hello World"
  fmt.Printf("%c",a[1])
}

布林

Go 語言中的布林與其他語言一致,關鍵字一樣是 bool,可以賦予值為 true 和 false。

範例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func main() {
  var a bool
  a = true
  fmt.Println("a =", a)

  b := false
  fmt.Println("b =", b)

  fmt.Println(true && true)
  fmt.Println(true && false)
  fmt.Println(true || true)
  fmt.Println(true || false)
  fmt.Println(!true)
}

如何宣告?

Golang 在變數宣告的一開始是不需要指定型別的。

  • 例如這樣即可:
1
var myName
  • 如果宣告時順便初始化可以這樣寫:
1
var myName = "Wayne"
  • 或是有更簡潔的寫法:
1
myName := "Wayne"
  • 當然也可以一次宣告很多個:
1
myName, myAge, isPassed := "Wayne", 18, true

Q:你可能會問,那我這樣寫會發生什麼事?

1
myName, myAge, isPassed := "Wayne", 18

A:那個 isPassed 將不會被初始化。

  • 如果想要指定變數型態的話,只需要在 var 後面加上指定型態即可:
1
2
3
4
5
var (
  myName string = "Wayne"
  myAge int = 18
  isPassed boolean = true
)

記得每個變數要不同行,中間不需要逗號分隔

最後有個比較特別的:常數(const),常數為一個固定值,當然之後在程式內是不允許被修改的。

1
2
const pi = 3.14
const hello_str = "Hello World"

迴圈

Go 只有一種迴圈 -「for」。

除了基本的「for」迴圈,沒有了「()」之外(甚至強制不能使用它們),它看起來跟 C 或者 Java 中做的一樣,而「{}」是必須的。

for

1
2
3
for i := 0; i <= 100; i++ {
  // do something
}

巢狀迴圈

1
2
3
4
5
for i := 0; i <= 100; i++ {
  for j := 0; j <= 100; j++ {
    // do something
  }
}

while 用法的 for 迴圈

1
2
3
4
i := 0
for i <= 10 {
  // do something
}

無窮迴圈

1
2
3
for {
  // do something
}

break

break 終止迴圈執行。

1
2
3
4
5
6
7
8
i := 0
for {
  // do something
  if i >= 100 {
    break;
  }
  i++
}

continue

有 break 當然也會有 continue,continue 就是略過本次的迴圈,直接進行下一輪:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
for {
  i++
  if i % 2 == 0 {
    continue
  }
  fmt.Print(i)
  if i > 50 {
    break
  }
}

if 條件判斷

if 語句除了沒有了「()」之外(甚至強制不能使用它們),看起來跟 C 或者 Java 中的一樣,而「{}」是必須的。

if

如果為 true 就執行,如果為 false 的話就不會執行:

1
2
3
4
5
6
7
8
9
if true {
  fmt.Println("This is true")
}
if false {
  fmt.Println("This line will not be executed")
}

// 輸出結果:
// This is true

&&:且
||:或

1
2
3
4
5
6
7
8
sex := "male"
age := 20
if sex == "male" && age >= 18 {
  fmt.Println("成年男性")
}

// 輸出結果:
// 成年男性

相反(!)

在 ture 或 false 之前加上!,會使結果顛倒,true 會變成 false,false 變成 true。

1
2
3
4
5
6
7
8
9
if !true {
  fmt.Println("This is !true")
}
if !false {
  fmt.Println("This is !false")
}

// 輸出結果:
// This is !false

if…else

1
2
3
4
5
6
7
8
9
myMoney := 100
if myMoney > 99 {
  fmt.Println("I can buy it")
} else {
  fmt.Println("I can't buy it")
}

// 輸出結果:
// I can buy it

if…else if

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
myMoney := 100
iceCream := 20
if myMoney-iceCream > 50 {
  fmt.Println("buy it!")
} else if myMoney-iceCream < 50 {
  fmt.Println("sorry, I won't buy")
}

// 輸出結果:
// buy it!

if…else if…else

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
myMoney := 100
ferrari := 450
bmw := 250

if myMoney > ferrari {
  fmt.Println("buy Ferrari")
} else if myMoney > bmw {
  fmt.Println("buy BMW")
} else {
  fmt.Println("buy Toyota")
}

// 輸出結果:
// buy Toyota

if 簡短的敘述

跟「for」一樣,「if」語句可以在條件之前執行一個簡單的語句,由這個語句定義的變數的作用範圍僅在「if」範圍之內。

(在最後的 return 語句處使用 v 看看。)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
	"fmt"
	"math"
)

func pow(x, n, lim float64) float64 {
	if v := math.Pow(x, n); v < lim {
		return v
	}
	return lim
}

func main() {
	fmt.Println(
		pow(3, 2, 10),
		pow(3, 3, 20),
	)
}

// 輸出結果:
// 9 20

switch 條件判斷

switch 在很多情況下可以取代 if 的功能,而且寫起來會更加簡潔。

範例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
myfriend := "Tony"
switch myfriend {
  case "Amy": 
    fmt.Println("Hi, Amy")
  case "Tony":
    fmt.Println("Hi bro")
  case "Jackey":
    fmt.Println("GO AWAY!")
}

// 輸出結果:
// Hi bro

要注意的是 case 中只需要:,並不需要 {} 來分隔。

default

如果都不在 case 裡的情況就會執行 default:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
myfriend := "Jason"
switch myfriend {
case "Amy":
  fmt.Println("Hi, Amy")
case "Tony":
  fmt.Println("Hi bro")
case "Jackey":
  fmt.Println("GO AWAY!")
default:
  fmt.Println("Nice to meet you, but who are you?")
}

// 輸出結果:
// Nice to meet you, but who are you?

fallthrough

在 case 中加入 fallthrough,會接著執行下一個 case。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
myfriend := "Amy"
switch myfriend {
case "Amy":
  fmt.Println("Hi, Amy")
  fallthrough
case "Tony":
  fmt.Println("Hi bro")
case "Jackey":
  fmt.Println("GO AWAY!")
default:
  fmt.Println("Nice to meet you, but who are you?")
}

// 輸出結果:
// Hi, Amy
// Hi bro

多重 case

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
myfriend := "Paul"
switch myfriend {
  case "Amy", "Emily": 
    fmt.Println("Hi, beautiful gril")
  case "Tony", "Paul":
    fmt.Println("Hi bro")
}

// 輸出結果:
// Hi bro

switch 沒有對象

這個情況就有點像是 if 的功能了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
myMoney := 100
switch {
  case myMoney > 500:
    fmt.Println("buy Ferrari")
  case myMoney > 250:
    fmt.Println("buy BMW")
  default
    fmt.Println("buy Toyota")
}

// 輸出結果:
// buy Toyota

BONUS(一):goto

Go 語言跟 C 語言一樣也有「goto」,但是不建議使用,會讓程式的結構變得很糟糕。

1
2
3
4
5
6
7
8
9
func main() {
  i := 0
HERE:
  fmt.Print(i)
  i++
  if i < 10 {
    goto HERE
  }
}

BONUS(二):defer

就許多現代語言而言,例外處理機制是基本特性之一,然而,例外處理是好是壞,一直以來存在著各種不同的意見,在 Go 語言中,沒有例外處理機制,取而代之的,是運用 deferpanicrecover 來滿足類似的處理需求。

defer

在 Go 語言中,可以使用 defer 指定某個函式延遲執行,那麼延遲到哪個時機?簡單來說,在函式 return 語句之後準備返回呼叫的函式之前,例如:

  • 延遲效果
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func myfunc() {
  fmt.Println("B")
}

func main() {
  defer myfunc()
  fmt.Println("A")
}

// 輸出結果:
// A
// B
  • 可在返回之前修改返回值
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func Triple(n int) (r int) {
	defer func() {
		r += n // 修改返回值
	}()

	return n + n // <=> r = n + n; return
}

func main() {
	fmt.Println(Triple(5))
}

// 輸出結果:
// 15
  • 變數的快照
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func main() {
  name := "go"
  defer fmt.Println(name) // 變數 name 的值被記住了,所以會輸出go

  name = "python"
  fmt.Println(name) // 輸出:python
}

// 輸出結果:
// python
// go
  • 應用
  1. 反序調用

如果有多個函式被 defer,那麼在函式 return 前,會依 defer 的相反順序執行,也就是 LIFO,例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func deferredFunc1() {
	fmt.Println("deferredFunc1")
}

func deferredFunc2() {
	fmt.Println("deferredFunc2")
}

func main() {
	defer deferredFunc1()
	defer deferredFunc2()
	fmt.Println("Hello, 世界")
}

// 輸出結果:
// Hello, 世界
// deferredFunc2
// deferredFunc1
  1. defer 與 return
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func f() {
  r := getResource()  // 0: 獲取資源
  // ......
  if ... {
    r.release()  // 1: 釋放資源
    return
  }
  // ......
  if ... {
    r.release()  // 2: 釋放資源
    return
  }
  // ......
  r.release()  // 3: 釋放資源
  return
}

使用 defer 後,不論在哪 return 都會執行 defer 後方的函數,如此便不用在每個 return 前寫上 r.release()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func f() {
  r := getResource()  // 0: 獲取資源

  defer r.release()  // 1: 釋放資源
  // ......
  if ... {
    // ...
    return
  }
  // ......
  if ... {
    // ...
    return
  }
  // ......
  return
}

以下是清除資源的範例:

 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
package main

import (
  "fmt"
  "os"
)

func main() {
  f, err := os.Open("/tmp/dat")
  if err != nil {
    fmt.Println(err)
    return;
  }

  defer func() { // 延遲執行,而且函式 return 後一定會執行
    if f != nil {
        f.Close()
    }
  }()

  b1 := make([]byte, 5)
  n1, err := f.Read(b1)
  if err != nil {
    fmt.Printf("%d bytes: %s\n", n1, string(b1))
    // 處理讀取的內容...
  }
}

BONUS(三):panic()

如果在函式中執行 panic(),那麼函式的流程就會中斷,若 A 函式呼叫了 B 函式,而 B 函式中呼叫了 panic(),那麼 B 函式會從呼叫了 panic 的地方中斷,而 A 函式也會從呼叫了 B 函式的地方中斷,若有更深層的呼叫鏈,panic 的效應也會一路往回傳播。

 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
package main

import (
  "fmt"
  "os"
)

func check(err error) {
  if err != nil {
    panic(err)
  }
}

func main() {
  f, err := os.Open("/tmp/dat")
  check(err)

  defer func() {
    if f != nil {
      f.Close()
    }
  }()

  b1 := make([]byte, 5)
  n1, err := f.Read(b1)
  check(err)

  fmt.Printf("%d bytes: %s\n", n1, string(b1))
}

如果在開啟檔案時,就發生了錯誤,假設這是在一個很深的呼叫層次中發生,若你直接想撰寫程式,將 os.Open 的 error 逐層傳回,那會是一件很麻煩的事,此時直接發出 panic,就可以達到想要的目的。

BONUS(四):recover

如果發生了 panic,而你必須做一些處理,可以使用 recover,這個函式必須在被 defer 的函式中執行才有效果,若在被 defer 的函式外執行,recover 一定是傳回 nil

如果有設置 defer 函式,在發生了 panic 的情況下,被 defer 的函式一定會被執行,若當中執行了 recover,那麼 panic 就會被捕捉並作為 recover 的傳回值,那麼 panic 就不會一路往回傳播,除非你又呼叫了 panic。

因此,雖然 Go 語言中沒有例外處理機制,也可使用 defer、panic 與 recover 來進行類似的錯誤處理。例如,將上頭的範例,再修改為:

 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
package main

import (
  "fmt"
  "os"
)

func check(err error) {
  if err != nil {
    panic(err)
  }
}

func main() {
  f, err := os.Open("/tmp/dat")
  check(err)

  defer func() {
    if err := recover(); err != nil {
      fmt.Println(err) // 這已經是頂層的 UI 介面了,想以自己的方式呈現錯誤
    }

    if f != nil {
      if err := f.Close(); err != nil {
        panic(err) // 示範再拋出 panic
      }
    }
  }()

  b1 := make([]byte, 5)
  n1, err := f.Read(b1)
  check(err)

  fmt.Printf("%d bytes: %s\n", n1, string(b1))
}

陣列

如何宣告陣列呢?很簡單!只要在變數宣告後面多加 [] 就可以了,裡面要填寫宣告的陣列數字。

1
var x [5]int

完整的範例:

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
  var x [5]int
  x[4] = 100
  fmt.Println(x)
}

這邊來稍微說明一下:

1
x[4] = 100

這一行的意思是說「x 的第五個元素賦予值 100」,之所以是第五個元素是因為,我們一開始用 var x [5]int 宣告了五個元素的陣列,而陣列的起始元素是第 0 個,從頭開始算 0, 1, 2, 3, 4 五個元素,所以 x[4] 表示的是第五個元素,如果要選擇第 0 個元素要寫 x[0]

接下來稍微看一個進階的例子來討論一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func main() {
	var x [4]float64
	x[0] = 23
	x[1] = 45
	x[2] = 33
	x[3] = 21

	var total float64 = 0
	for i := 0; i < 4; i++ {
		total += x[i]
	}
	fmt.Println(total / 4)
}

這邊一樣是宣告五個元素的陣列,x[0] ~ x[4] 的部分是初始化數值的動作,後面 for 迴圈是走訪所有個元素並且加到 total 這個變數中,最後在 Println 的部分再除以 4 計算平均。雖然這個程式沒有問題,但是沒有彈性,如果我今天加入六個元素、七個元素,你就必須要更改三個地方(宣告、for 迴圈、fmt.Println),所以我們稍微將它更改一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var x [4]float64
x[0] = 23
x[1] = 45
x[2] = 33
x[3] = 21

var total float64 = 0
for i := 0; i < len(x); i++ {
  total += x[i]
}
fmt.Println(total / len(x))

我們利用內建的函式 len() 來計算陣列的數量,這樣子如果元素有修改,我們就只需要修改一個部分就可以了,剩下的 len() 會自己去計算陣列的數量。但是!!!這個程式執行會有問題,如果你執行程式碼會看到錯誤:

1
invalid operation: total / 4 (mismatched types float64 and int)

這是因為 total 變數和 len() 計算完成後的型態不同,一個是 float64 另一個則是 int,所以發生錯誤,你必須要讓它強制轉型才可以執行。

1
fmt.Println(total / float64(len(x)))

另外 for 迴圈有另外一種內建的寫法可以走訪每個陣列,就是利用 range ,但是他預設會有兩個回傳值,一個是鍵一個是值。

1
2
3
4
5
var total float64 = 0
for i, value := range x {
  total += value
}
fmt.Println(total / float64(len(x)))

這個程式乍看之下沒有問題,但是我們都知道 Go 沒有使用的函式、變數,被 compile 看到連編譯都不給編,會出現「i declared and not used」錯誤,但是這邊我們真的不需要「i」這個鍵怎麼辦?我們可以利用「_」這個佔位符來取代。

1
2
3
4
5
var total float64 = 0
for _, value := range x {
  total += value
}
fmt.Println(total / float64(len(x)))

讓我們來稍微檢視一下最後修改的版本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func main() {
  var x [4]float64
  x[0] = 23
  x[1] = 45
  x[2] = 33
  x[3] = 21

  var total float64 = 0
  for _, value := range x {
    total += value
  }
  fmt.Println(total / float64(len(x)))
}

看起十分的精簡又方便維護!但是就只有這樣了嗎?我個人覺得初始化的部份還是太長了,就讓我們來簡化它!

1
x := [4]float64{23, 45, 33, 21}

很短對不對!不過它很不方便閱讀,所以你也可以改成:

1
2
3
4
5
6
x := [4]float64{
  23,
  45,
  33,
  21,
}

注意最後一個逗點要留著喔!不然編譯會出錯,你不要覺得這個逗點很奇怪,一切都是有原因的,原因就在於方便維護!


Slice(切片)

在建立陣列的時候要指定元素的大小,但是!我們今天如果不知道要多大怎麼辦?陣列可以不輸入元素大小嗎?答案是可以!不過這個方法會有問題!而且他的型態不是陣列,而是 Slice。

建立 Slice

1
var x []float64

夠簡單了吧!其實就只是指陣列不寫元素大小而已。

Slice 有兩種數值,一個是容量一個是長度。這兩個有什麼不同呢?其實容量就是最大能裝多少,而長度不能超過容量。

那我們要怎麼設定長度跟容量呢?

1
x := make([]float64, 5)

這是標準的建立 slice 的方法,這裡設定一個長度和容量都是 5 的 slice 變數 x,這時候你會問:為什麼我只有指定一個 5 ,它卻幫我把長度跟容量設定為 5 呢?這是因為這裡設定的 5 其實是長度,容量則是系統自動配置,會符合最適當大小,所以也是 5。

如果要自訂容量大小,只要再多一個參數就可以設定囉:

1
x := make([]float64, 5, 10)

我們還可以利用另外一個方法建立 Slice,[low : high] 這個方法來參考別的 Slice 或是陣列來建立 Slice。

1
2
arr := [5]float64{1,2,3,4,5}
x := arr[0:5]

需要特別注意的點是 high 表示的是結束的點,而不是它的元素位置喔!

以上面的例子來說 arr[0:5] 其實是指 [1,2,3,4,5],如果是 arr[1:4] 則是 [2,3,4]

另外在某些情況下 low 跟 high 是可以省略的,像是 arr[0:5] 這樣子就可以省略成 arr[:5]

如果省略成 arr[0:] 則是相當於 arr[0: len(arr)]

是的,你發現了嗎?你當然可以省略成 [:],它相當於 arr[0:len(arr)]

Append 與 Copy

Slice 有兩個一定要學會的函式,一個是 Append 另一個是 Copy,讓我們逐一看一下例子:

1
2
3
4
5
func main() {
  slice1 := []int{1,2,3}
  slice2 := append(slice1, 4, 5)
  fmt.Println(slice1, slice2)
}

這邊使用 append 把 4、5 加入到 Slice1 中,並且賦予 Slice2。如果要更改原本的內容則是要這樣寫:

1
slice1 = append(slice1, 4, 5)

只要重新覆蓋掉 slice1 就可以了,但是要特別注意!不可以加冒號!冒號是宣告的時候用的短語法。


Map

在 Golang 中,有沒有一個類似 JSON 陣列的方法、型態呢?你可以使用 Map!

建立與使用 Map

1
var x map[string]int

跟 Array 還有 Slice 有點相似對吧?我這邊說明一下:

  • x 是變數名稱
  • map 這個是建立 Map 的時候用的關鍵詞彙不能省略
  • 括弧中的 string 表示鍵的型態int 則是值的型態

我們來嘗試建立看看一個 Map:

1
2
3
var x map[string]int
x["key"] = 10
fmt.Println(x)

什麼!?居然出現 Panic,這個是什麼?

1
panic: runtime error: assignment to entry in nil map

我稍微省略了一些錯誤的 log ,讓我們聚焦在最重要的這一句話上面,他的意思是說不能賦予值到 nil map 上面。原來我們用 var x map[string]int 建立 Map 的時候沒有初始化它,所以它會建立一個 nil map,而 nil map 不能使用鍵,所以導致這樣的錯誤,那初始化怎麼寫呢?

1
2
3
x := make(map[string]int)
x["key"] = 10
fmt.Println(x)

如果你只是要印出鍵的數值:

1
fmt.Println(x["key"])

當然 string 不是唯一的選項,你也可以用 int。

1
2
3
x := make(map[int]int)
x[1] = 10
fmt.Println(x[1])

這樣就可以使用數字當作鍵來用。

另外可以新增,但是要怎麼刪除呢? Go 有內建函式可以使用:

1
delete(x, 1)

除了新增刪除,如果今天我想要知道 map 有多少元素呢?

我們之前介紹過得 len() 也可以用的喔,如果只是剛建立沒有給值會是 0 ,如果像是前面的 x[1] = 10 這樣賦予值就會變成 1,以此類推。

1
len(x)

如果鍵沒有宣告?

如果我們今天印出的鍵值沒有宣告,例如:

1
2
3
4
5
elements := make(map[string]string)
elements["H"] = "Hydrogen"
elements["He"] = "Helium"

fmt.Println(elements["Al"])

通常我們要印出不存在的,在編譯的時候會報錯,但是這邊你執行後你會發現 Map 不會。

那 Go 有沒有方法可以判斷它是否是空的呢?當然有的!

1
2
name, ok := elements["Al"]
fmt.Println(name, ok)

這樣會印出什麼呢?它會印出 false,因為他是空值,但是你會發現明明 map 就回傳兩個返回值,為什麼印出來只有 false?原因是 name 其實對應到 value,但是值是空值,所以就沒有印了,而 ok 是返回它有沒有值。

我們再來利用這個語法來做點進階的應用:

1
2
3
if name, ok := elements["Al"]; ok {
  fmt.Println(name, ok)
}

這裡的意思是說「Al 是否有值呢?如果有的話請印出」。

如果覺得前面初始化的太長了,你可以試試這種簡短的寫法:

1
2
3
4
elements := map[string]string{
  "H": "Hydrogen",
  "He": "Helium",
}

當然我們都知道 JSON 格式可以是巢狀的,Go 當然一定也可以囉!

但是要怎麼做呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func main() {
  elements := map[string]map[string]string{
    "H": map[string]string{
      "name":"Hydrogen",
      "state":"gas",
    },
    "He": map[string]string{
      "name":"Helium",
      "state":"gas",
    },
  }

  if el, ok := elements["He"]; ok {
    fmt.Println(el["name"], el["state"])
  }
}

Struct

Struct 是一個可以自定義型態的功能。

Struct 就是有點類似 OOP(物件導向) 的概念。比如說我今天想要建立一個型態"人",那人有姓名和身高,這時候就可以使用 Struct。

1
2
3
4
type person struct {
  name
  height
}

實例化:

1
p := person{"Tony", 169}

{} 內的順序依定義 struct 的順序,或是可以指定項目初始化:

1
p := person{name: "Tony", height: 169}

如果想要存取 struct 裡面的內容,只要在後面加 . 就好了;比如說我想存取 person 的 name:

1
fmt.Println(person.name)

多重定義

比如說我的人比較多了,想要建立群組,裡面有群組名稱及人:

1
2
3
4
5
6
7
8
9
type person struct {
  name string
  height int
}

type group struct {
  name string
  person
}

實例化:

1
g := group{"LINE", person{name: "Emily", height: 158}}

這時候就算是 struct 裡面又包了 struct。


函式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import "fmt"

func add(x int, y int) int {
  return x + y
}

func main() {
  fmt.Println(add(42, 13))
}

// 輸出結果:55

函數可以沒有參數接受多個參數

在這個例子中,“add” 接受兩個 int 類型的參數(注意類型在變數之後)。

return 代表傳回的數值為何?這裡表示傳回 x + y 也就是 42 + 13 ,所以結果會是 55。

當兩個或多個連續的函數命名參數是同一類型,則除了最後一個類型之外,其他都可以省略:

1
x int, y int

可以縮寫成:

1
x, y int

也就是說上面的程式碼可以改成:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main
 
import "fmt"
 
func add(x, y int) int {
  return x + y
}

func main() {
  fmt.Println(add(42, 13))
}

// 輸出結果:55

多數值的返回

函數可以返回任意數量的返回值,這個函數返回了兩個字串:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main
 
import "fmt"
 
func swap(x, y string) (string, string) {
  return y, x
}
 
func main() {
  a, b := swap("hello", "world")
  fmt.Println(a, b)
}

// 輸出結果:world hello

命名返回值

在 Go 中,函數可以返回多個「結果參數」,而不僅僅是一個值。它們可以像變數那樣命名和使用。

如果命名了返回值參數,一個沒有參數的 return 語句,會將當前的值作為返回值返回。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import "fmt"

func split(sum int) (x, y int) {
  x = sum * 4 / 9
  y = sum - x
  return
}

func main() {
  fmt.Println(split(17))
}

// 輸出結果:7 10

以這個程式碼為例,sum int 表示宣告整數 sum,將參數 17 放入 sum 中,x, y int 宣告整數 x,y 在下面使用,由於 return 沒有設定返回值,這邊程式就將 x,y 都回傳了,所以結果會出現 7 10


Goroutine

Go 很酷的特色 Goroutine,他類似於其他語言的 Thread,意思即一支程式同時進行好幾個小程式。

要使用 Goroutine 非常的簡單,只要一個字,一個很熟悉的字,「go」。

讓我們看看下面這個簡單的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
	"fmt"
)

func f(n int) {
	for i := 0; i < 10; i++ {
		fmt.Println(n, ":", i)
	}
}

func main() {
	go f(0)
}

你執行後你會發現什麼東西都沒有印出!

不是 Go 有問題,因為使用 goroutine 是平行處理的,所以在還沒開始印 n 之前,main 這個主要的函式已經結束了。

我們使用一下內建的 time 函式,讓 main 函式等一下,讓 goroutine 跑完。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import (
	"fmt"
	"time"
)

func f(n int) {
	for i := 0; i < 10; i++ {
		fmt.Println(n, ":", i)
	}
}

func main() {
	go f(0)
	time.Sleep(time.Second * 1) // 暫停一秒鐘
}

你就可以看到印出 1 ~ 10 了。


WaitGroup

有鑑於剛剛背景 Thread 所執行的項目都還沒開始程式就結束了,WaitGroup 的功能即為等待這個 Group 的項目執行完再繼續進行。

首先,因為 WaitGroup 是包含在 package sync 裏頭的,所以需要引入一下:

1
import "sync"

宣告一個 WaitGroup 變數:

1
var wg sync.WaitGroup

當 WaitGroup 裏頭的空間等於 0 的時候就會繼續進行主程式,下面會介紹一些操作方式。

Add()

增加 WaitGroup 裡可以容納個 Thread。

1
wg.Add(2)

Wait()

等待 WaitGroup 裡的 Thread 執行完畢再繼續進行。

1
wg.Wait()

Done() 或是 Add(-1)

使 WaitGroup 的容量 - 1。

1
2
3
wg.Done()
// or
// wg.Add(-1)

有了這些基本操作後,我們可以改良剛剛的程式:

 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
package main

import (
  "fmt"
  "sync"
)

func foo() {
  for i := 1; i <= 10; i++ {
    fmt.Println("Foo: ", i)
  }
  wg.Done()
}

func bar() {
  for i := 1; i <= 10; i++ {
    fmt.Println("Foo: ", i)
  }
  wg.Done()
}

var wg sync.WaitGroup

func main() {
  wg.Add(2)
  go foo()
  go bar()
  wg.Wait()
}

Sleep

讓程式暫停。

這個功能是放在 package time 裡面的,所以又要引入了。

1
2
3
import "time"

time.Sleep(5 * time.Millisecond)

這個範例是暫停五秒鐘。


Race condition

有了 go 這個好用的東西可以分成好幾個 Thread 同時進行,那就有可能會有同時搶奪資源的情況。比方說兩個 Thread 同時都要讀取並修改同一個變數,拿一個情境題來舉例:

如果今天同一個銀行的網路銀行有提款功能,而有人同時在兩台電腦都登入了要提款,兩台電腦都送出了提領 1000 的請求會怎樣呢?

 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
package main

import (
  "fmt"
  "sync"
  "time"
)

func withdraw() {
  balance := money
  time.Sleep(3000 * time.Millisecond)
  balance -= 1000
  money = balance
  fmt.Println("After withdrawing $1000, balace: ", money)
  wg.Done()
}

var wg sync.WaitGroup
var money int = 1500

func main() {
  fmt.Println("We have $1500")
  wg.Add(2)
  go withdraw() // first withdraw
  go withdraw() // second withdraw
  wg.Wait()
}

到最後大家會發現 1500 提領了兩次 1000 還剩 500?為甚麼呢?

因為在第一次提領的時候,系統先讀取餘額為 1500 元,同時第二台電腦也登入了餘額也是顯示為 1500 元,這時候就是因為兩邊同時搶著讀取餘額的原因,所以第一次提領 1000 元時回報給系統"餘額剩 500 元",第二台領了 1000 元也回報系統"餘額剩 500 元"。

不過 Golang 的編譯器可以檢查是不是有 race condition,只要在平常執行 go run 後面加上參數 -race 即可。

1
go run -race main.go

Mutex、Lock()、Unlock()

那要如何避免這樣的情況發生呢?Mutex 可以解決上面這樣的問題。這個也是在 package sync 裏頭。

1
2
3
4
import "sync"

// 宣告一個 Mutex 變數
var mu sync.Mutex

當使用 mu.Lock() 的時候,之後所用到的變數就會上鎖,只有在使用中的 Thread 可以存取,其他都需要等到釋放後才能存取。

1
mu.Lock()

釋放 lock 變數:

1
mu.Unlock()

所以我可以將剛剛 withdraw() 改良:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func withdraw() {
  mu.Lock()
  balance := money
  time.Sleep(3000 * time.Millisecond)
  balance -= 1000
  money = balance
  mu.Unlock()
  fmt.Println("After withdrawing $1000, balace: ", money)
  wg.Done()
}

因為 Lock()Unlock() 通常都會一起出現,所以有些人會這樣寫:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func withdraw() {
  {
    mu.Lock()
    balance := money
    time.Sleep(3000 * time.Millisecond)
    balance -= 1000
    money = balance
    mu.Unlock()
  }
  fmt.Println("After withdrawing $1000, balace: ", money)
  wg.Done()
}

全部的程式碼:

 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
package main

import (
  "fmt"
  "sync"
  "time"
)

func withdraw() {
  mu.Lock()
  balance := money
  time.Sleep(3000 * time.Millisecond)
  balance -= 1000
  money = balance
  mu.Unlock()
  fmt.Println("After withdrawing $1000, balace: ", money)
  wg.Done()
}

var wg sync.WaitGroup
var money int = 1500
var mu sync.Mutex

func main() {
  fmt.Println("We have $1500")
  wg.Add(2)
  go withdraw() // first withdraw
  go withdraw() // second withdraw
  wg.Wait()
}

如此一來就解決 race condition 的問題了!


Channel

Channel 也是 Golang 非常特別的特色。

要建立一個 Channel 很簡單,make(chan string) 這樣就可以了。

1
c := make(chan int)

Send

要將東西送到 channel 就把資料指向 channel 變數就好:

1
c <- 5

Receive

就將箭頭往外指。

1
x := <-c

Close()

當 Channel 被 Close 之後,就只能讀取不能寫入了:

1
close(c)

當變數傳遞

channel 其實就是一個變數,在 function 裡是可以傳入傳出的。

1
2
3
4
5
6
7
8
9
// 傳入
func foo(c chan int) {
  // do something
}

// 傳出
func foo() {
  // do something
}

這邊簡單建立了一個 message 的 channel,可以傳輸字串,然後用 go 來 call goroutine 執行函式,然後 msg 負責接收 messages 的傳輸資料,goroutine 執行的函式裡面傳 “ping” 到 messages 這個 channel 裡面,再由 message 傳給 msg 變數印出。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import "fmt"

func main() {

	messages := make(chan string)

	go func() { messages <- "ping" }()

	msg := <-messages
	fmt.Println(msg)
}

再講個範例可能會比較容易懂:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
	"fmt"
)

func main() {
	c := make(chan int) 

  // 在 goroutine 之下執行這個 function
	go func() { 
		for i := 0; i < 10; i++ {
			c <- i 
		}
		close(c)
	}()

	for n := range c {
		fmt.Println(n) 
	}
}

在這裡 go func() 將 i 傳入 c,下面的 for 會等待 c 有什麼時候有新東西,一有新東西他就會 print 出來!

很簡單直覺對吧!透過這個方法就可以簡單的讓 Goroutine 可以溝通!


Select

Channel 有一個類似 Switch 的流程控制「Select」,它只能應用於 Channel 讓我們一起來看看。

 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
package main

import "time"
import "fmt"

func main() {

  c1 := make(chan string)
  c2 := make(chan string)

  go func() {
    time.Sleep(time.Second * 1)
    c1 <- "one"
  }()
  go func() {
    time.Sleep(time.Second * 2)
    c2 <- "two"
  }()

  for i := 0; i < 2; i++ {
    select {
    case msg1 := <-c1:
      fmt.Println("received", msg1)
    case msg2 := <-c2:
      fmt.Println("received", msg2)
    }
  }
}

這邊用 go 建立兩個 goroutine 分別將 one 和 two 傳給 c1、c2,下面主函式的 for 回會將 1、2 透過 select 流程控制來接收 channel 的訊息再印出。

是不是很簡單,這樣子就可以更有效的控制 channel 了。


錯誤處理

有時候程式的錯誤是在預料之內的,防範也防範不了,但總不能因為一個錯誤就讓整支程式停下來吧?因此就需要 Error Handling。如果有學過其他程式語言可能會覺得用法跟 try…cache 有點不同。

如何使用?

其實滿簡單的,只要知道所使用的 function 有回傳 error 就可以使用,通常在使用手冊上會看到,以 HTTP GET 來說:

1
2
3
4
5
resp, err := http.Get("http://example.com/")
// ...
resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf)
// ...
resp, err := http.PostForm("http://example.com/form", url.Values{})

官方使用手冊就有回傳 err 的參數,意思就是錯誤內容會傳到 err 內。

那就 HTTP GET 來舉例,如果有 Get 到東西的話就會存到 resp,沒有的話就會把錯誤訊息存到 err:

1
2
3
4
resp, err := http.Get("http://ithelp.ithome.com.tw/")
if err != nil {
	fmt.Println(err)
}

錯誤內容 err 是可以直接 Print 出來的!

Fatalln()

Fatalln() 這個 function 就是兩行 code 的組合,這個 function 是包在 “log” 中的。

1
2
3
4
5
import "log"

// Print 出錯誤內容後結束程式
fmt.Println(err)
os.Exit(1)

淺談 package

package 可以將一些常常會用的程式碼獨自建立一個檔案,所以在同一個資料夾內可以有好幾個 package 檔案。某方面來說也是便於管理程式碼。

package 的有效範圍

在同一個 package 中可以把這些檔案看作是在同一個檔案內。

比如說 package person 中第一個檔案 personName.go:

personName.go
1
2
3
package person

var MyName = "Wayne"

package person 中第二個檔案 sayHelloTo.go:

sayHelloTo.go
1
2
3
4
5
6
package person

func SayHelloTo(s string) string {
	str := "Hello " + s + "!"
	return str
}

這兩個檔案都在 package person 中,所以可以把它視為這樣:

1
2
3
4
5
6
7
8
package person

var MyName = "Wayne"

func SayHelloTo(s string) string {
	str := "Hello " + s + "!"
	return str
}

該如何使用 package?

只要在程式一開始 import 就行了,常用的 fmt 就是一個標準 package。

要注意的是路徑是從 $GOPATH/src/ 為起始目錄的相對路徑。

1
2
3
4
import {
  fmt
  "github.com/xxx"
}

大小寫的差異

在 package 中變數或函式的命名開頭字元大小寫是有差異的:

  • 大寫開頭: 在 package 之外可見,稱作 exported
  • 小寫開頭: 僅限於 package 內使用,稱作 unexported

注意:這概念跟其他語言的 public、private 很像,但是在 GO 的領域通常還是講 exported、unexported。

舉個例子來說:

alien.go
1
2
3
4
package alien

// name start with capital will be exported in another file
var AlienName = "Wayne"

這邊的 AlienName 大寫開頭 - exported

所以在其他檔案中只要 import 就可以使用:

main.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import (
	"fmt"
	"alien"
)

func main() {
	fmt.Println(alien.AlienName)
}

如果是 function 也是一樣:

person.go
1
2
3
4
5
6
package person

func SayHelloTo(s string) string {
	str := "Hello " + s + "!"
	return str
}
main.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import (
	"fmt"
	"person"
)

func main() {
	fmt.Println(person.SayHelloTo("Wayne"))
}

變數的可視範圍

變數的宣告其實也是門學問,很多新手會想說"全部都宣告全域變數就好了啊",但為了安全性、可讀性以及有效的執行最小權限原則(即為要用到的變數才看的到,用不到的就不需要看到),這裡就仔細地跟大家講解。

package

同一個 package 可以視為程式碼都在同一個檔案內。

括號內的變數 {}

括號內的變數只要是同樣在括號內都能使用,括號外則不行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main 

import "fmt"

func main() {
  x := 1
  foo() // 錯誤,因為 x 屬於 main 中的變數
}

func foo() {
  fmt.Println(x)
}

全域變數

這個比較猛,意思就是在程式中都能讀到他,每個地方的操作都會改變他的值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

import "fmt"

var x = 0

func xPlus() {
  x++
}

func main() {
  x++ // x = 1
  xPuls() // x = 2
}

順序

宣告的順序當然也很重要,宣告之後才可用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import "fmt"

func main() {
  x := 5
  fmt.Println(x)
  fmt.Println(y) // 這樣是找不到 y 的
  y := 10
}

變數和函式名稱相同

這樣用是不會出錯,但是不建議,日後 debug 可能造成麻煩。


Pointer

將變數直接指向記憶體位置就叫做 Pointer,要修改內容就直接到該記憶體位置修改。

基本操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import "fmt"

func main() {
  var p *int // 宣告 p 是一個 int 的指標,但此時他要指向哪還不知道
  a := 10 // a 佔用了一個記憶體空間
  
  p = &a // 將 p 指到 a 的記憶體位置
  
  fmt.Println(p) // p 所指到的記憶體位置
  fmt.Println(*p) // * 代表顯示該記憶體位置的值
}

function 的運作

有了 Pointer 的概念,就比較好理解 function 是怎麼傳值的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import "fmt"

func foo(x int) {
  fmt.Println(&x) // function 內 x 的記憶體位置
}

func main() {
  a := 10
  fmt.Println(&a) // main 裡面 a 的記憶體位置
  foo(a)
}

結果會看到兩個記憶體位置是不一樣的。代表在 function 傳值過去後,function 複製該值到另一個空間來操作,對於原本 main 裡面的值是不會影響的。

function 傳指標

當然,想要在傳指標到 function 內操作也是可以的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import "fmt"

func foo(x *int) {
  fmt.Println(x) // function 內 x 的記憶體位置
}

func main() {
  a := 10
  fmt.Println(&a) // main 裡面 a 的記憶體位置
  foo(&a)
}

可以看到傳指標過去後,兩邊操作的是同一個東西。


測試

寫了這麼多小程式,甚至也自己打包了成 Package 了,那 Go 有提供自動化測試的方法嗎?測試程式是否正常?讓我們一起來看看 Go 的測試方法。

首先我們先寫一個簡單的 Package 來看看:

testing.go
1
2
3
4
5
6
7
8
9
package math

func Average(xs []float64) float64 {
  total := float64(0)
  for _, x := range xs {
    total += x
  }
  return total / float64(len(xs))
}

這個很簡單,這裡簡單建立一個叫 math 的 Package,這是一個簡單算平均的 Package,將讀取進來的浮點數陣列一個一個用 for 讀取進來累加,然後在返回總和除總數的結果。

main.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package math

import "testing"

func TestAverage(t *testing.T) {
  var v float64
  v = Average([]float64{1,2})
  if v != 1.5 {
    t.Error("Expected 1.5, got", v)
  }
}

然後我們來寫另外一個檔案,這個是測試的寫法,建議檔名取 math_test.go,因為前面的 Package 取名叫做 math.go。

一開始先宣告測試用的變數 t,然後引用 Average 函數將 1、2 拿進去計算,如果答案不對利用 t 返回錯誤,使用起來相當的直覺。


操作檔案

讓我們先看看下面這個簡單的範例:

 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
package main

import (
	"fmt"
	"os"
)

func main() {
  // 首先我們先利用內建的函式庫 os ,利用 open 方法來開起檔案:
  // file 為開啟後的檔案,用來做後續的操作,err 這個是當開啟失敗的時候返回的錯誤訊息
	file, err := os.Open("test.txt")
	if err != nil {
		return
	}
  // 這一行是使用 defer 來確保檔案有被正確的關閉用的
	defer file.Close()

	stat, err := file.Stat()
	if err != nil {
		return
	}

	bs := make([]byte, stat.Size())
	_, err = file.Read(bs)  // 一般情況下會是 nil,如果你不需要 error 訊息你可以使用佔位符來取代

  // 這邊則是說如果 err 不是 nil 那就返回,當然你也可以輸入一些你要設定的除錯訊息
	if err != nil {
		return
	}

	str := string(bs)
	fmt.Println(str)
}

時間

時間是一般開發者常常會用到的功能,Go 已經內建得很齊全了,我們一起來看看吧!

 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
package main

import (
	"fmt"
	"time"
)

func main() {
	p := fmt.Println

  // 獲取目前的時間
	now := time.Now()
	p(now)

  // 獲取自訂時間
	then := time.Date(
		2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
	p(then)

	p(then.Year())
	p(then.Month())
	p(then.Day())
	p(then.Hour())
	p(then.Minute())
	p(then.Second())
	p(then.Nanosecond())
	p(then.Location())

	p(then.Weekday())

	p(then.Before(now))
	p(then.After(now))
	p(then.Equal(now))

	diff := now.Sub(then)
	p(diff)

	p(diff.Hours())
	p(diff.Minutes())
	p(diff.Seconds())
	p(diff.Nanoseconds())

	p(then.Add(diff))
	p(then.Add(-diff))
}

如果要計算時間差呢?只要使用 Sub 方法就可以計算囉!詳細 time 用法可參考網址


操作字串

 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
package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(
		// true
		strings.Contains("test", "es"),

		// 2
		strings.Count("test", "t"),

		// true
		strings.HasPrefix("test", "te"),

		// true
		strings.HasSuffix("test", "st"),

		// 1
		strings.Index("test", "e"),

		// "a-b"
		strings.Join([]string{"a", "b"}, "-"),

		// == "aaaaa"
		strings.Repeat("a", 5),

		// "bbaa"
		strings.Replace("aaaa", "a", "b", 2),

		// []string{"a","b","c","d","e"}
		strings.Split("a-b-c-d-e", "-"),

		// "test"
		strings.ToLower("TEST"),

		// "TEST"
		strings.ToUpper("test"),
	)
}
  • Contains: 藉由 Contains 可以知道字串中是否包涵哪些字串
  • Count: 用來計算一個字串中的某個字元有幾個
  • HasPrefix、HasSuffix: 用來確認字頭字尾始否有包函某些字串
  • Index: 用來計算指定的字元是字串中的第幾個元素
  • Join: 用來合併成字串,而中間可以嵌入指定的字元
  • Repeat: 重複字串
  • Split: 利用特定字元來拆開字串,拆開的字會放進陣列
  • ToLower: 用來把字串都換成小寫,當然他只有英文XD
  • ToUpper: 用來把字串都換成大寫,當然他只有英文XD

Hash 雜湊

雜湊 (Hash) 是現在很常見的應用,可以用來驗證檔案的正確性、加密等。

範例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import (
	"crypto/sha1"
	"fmt"
	"hash/crc32"
)

func main() {
	h := crc32.NewIEEE()
	h.Write([]byte("test"))
	v := h.Sum32()
	fmt.Println(v)

	i := sha1.New()
	h.Write([]byte("test"))
	bs := i.Sum([]byte{})
	fmt.Println(bs)
}

這邊簡單舉了一般的 Hash 跟 Crypto 的函式用法,當然還有很多種類可以自己看。

這邊 crc32 的部份用 NewIEEE 方法來建立 checksum,然後利用 write 這個內建的 interface 來寫入要算的值,這時候 h 已經是我們要的 Hash 值了,後面這邊在利用 sum32 來返回成我們可以閱讀的 uint32 的值。

sha1 的部份基本上很類似,應該不用多說,馬上可以看懂。

詳細請參考:


List

有上過資料結構的人都知道 List,Go 有實作喔!不過他不是基礎型別,需要特別引入,讓我們一起來看看。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import (
	"container/list"
	"fmt"
)

func main() {
	var x list.List
	x.PushBack(1)
	x.PushBack(2)
	x.PushBack(3)

	for e := x.Front(); e != nil; e = e.Next() {
		fmt.Println(e.Value.(int))
	}
}

先是建立 List x 然後利用 PushBack 函式從後面填入,最後利用 For 迴圈將一個一個取出。

除了 List 外還有 Heap 喔!有興趣的可以參考看看。

詳細請參考:


GET / POST

錯誤處理的時候有提到 HTTP GET、POST,這邊再來說明一下。

舉個例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func httpGet() {
	resp, err := http.Get("https://tw.yahoo.com/")
	if err != nil {
		// handle error
	}

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		// handle error
	}

	fmt.Println(string(body))
}

你可以看到,這是一個 function,我們利用 http.get 的方法來將 request 送給 Yahoo,Get 方法會返回兩個數值,一個是 response,另一個是 error,後面利用 defer 來確定是否有收到,然後將 response 裡的 Body 也就是網頁內容讀取出來,然後轉成字串印出。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func httpPost() {
	resp, err := http.Post("https://tw.yahoo.com/",
		"application/x-www-form-urlencoded",
		strings.NewReader("name=test"))
	if err != nil {
		fmt.Println(err)
	}

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		// handle error
	}

	fmt.Println(string(body))
}

POST 其實大部份就跟 GET 相同,但是不一樣的是 POST 通常傳的資料會比較多,所以這邊可以在後面的參數加上你要傳給伺服器的參數,另外需要特別注意的是,必須使用 application/x-www-form-urlencoded 才能正確的傳值喔!


Martini

我們來玩玩 Framework 吧!

安裝 Martini

只要用 go get 就可以在本地引入 lib 囉!

1
go get github.com/go-martini/martini  

然後我們就可以開始使用官方的 simple code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import "github.com/go-martini/martini"

func main() {
	m := martini.Classic()
	m.Get("/", func() string {
		return "Hello world!"
	})
	m.Run()
}

這是一個簡單利用 Martini 來啟動 Server 的範例程式碼,首先引入 Martini,後面宣告 martini 為變數 m,後面在使用 router 來解析網址 「/」,再來我們再跟目錄網只要做什麼事情呢?就是返回 “Hello world”!規則都設定完了,那我們就用 Run 來啟動伺服器。

那我們的 Code 都寫完了,那我們就用 go run 來執行它,例如你的 code 檔名取叫 server.go,那你只要使用:

1
go run server.go

就可以執行囉,接下來你就可以開瀏覽器來看看是否有顯示「Hello world!」,網址請打 localhost:3000,3000 為 Martini 的預設 port。


Router、Template

簡單使用 Martini 的 Router 和 Template 做網站開發!

一開始我們會需要另外一個 lib,因為 Martini 的 lib 只有基本伺服器的工具,沒有包函 View Render,所以我們會需要使用 View Render 來操作 Template。

所以我們先使用 go get 來下載:

1
go get github.com/martini-contrib/render

這樣我們就可以引用 lib 了!

 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
package main

import (
	"github.com/go-martini/martini"
	"github.com/martini-contrib/render"
)

func main() {
	m := martini.Classic()

	m.Get("/hello/:name", func(params martini.Params) string {
		return "Hello " + params["name"]
	})

	m.Use(render.Renderer(render.Options{
		Layout: "layout",
	}))

	type Member struct {
		Id   int
		Name string
		Sex  string
	}
	member := Member{
		Id: 1, Name: "Negaihoshi", Sex: "Male",
	}

	m.Get("/", func(r render.Render) {
		r.HTML(200, "hello", member)
	})

	m.Run()
}

首先 m.Get 是使用 Get 方法來獲取 response,如同前面的方法,不過稍微不同的是我們這邊稍微做一點進階的操作,我們在 router 的 rule 這邊打上 /hello/:name,這個是什麼意思呢?意思是網址 /hello/ 下網站會看 :name 這個變數來做操作,所以下面就要寫我們要做什麼?而這邊我們返回 "Hello " + params["name"] 而 params 這邊就是用來解析引數的語法。

如果我今天輸入網址 /hello/:Negaihoshi,那我網站就會出現「Hello Negaihoshi」。

下面這邊我們利用 Use 方法來設定 Options,而這邊設定的是我們要使用 layout,並且指定 layout 的位置,而我們這邊寫 “layout”,指的是 templates/layout.tmpl 這個檔案。

往後面看這邊就簡單了,用了前面學過得 stuct 來存放資料,並且使用 Get 方法,當有人瀏覽根目錄的時候,將 member 傳入 hello.tmpl 檔。

1
2
3
<title></title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
{{ yield }}

這邊其實很簡單就是使用 yield 來引入入其他的 tmpl 檔。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<table class="table table-hover">
  <thead>
    <tr>
      <td>ID</td>
      <td>Name</td>
      <td>Sex</td>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>{{ .Id }}</td>
      <td>{{ .Name }}</td>
      <td>{{ .Sex }}</td>
    </tr>
  </tbody>
</table>

這邊則是印出我們要的數值,而需要用「.」來操作。


資料庫

前面學會了怎麼簡易的使用 martini,這邊我們來使用看看資料庫。

首先我們要安裝 mysql 的驅動程式套件,我們使用 GitHub 上比較知名的的項目來做。

在終端機輸入:

1
go get github.com/go-sql-driver/mysql

這樣就可以使用囉!

當然,我們需要引入它才能使用:

1
2
3
4
import "database/sql"
import \_ "github.com/go-sql-driver/mysql"

db, err := sql.Open("mysql", "user:password@/dbname")

user 和 password 跟 dbname 就要換成你要連接的資料庫的資料囉!

資料都輸入正確就可以連接資料庫了。

記得加入很重要的 error,這樣連接失敗才能正確的回報哦!

1
2
3
if err != nil {
  panic(err.Error())
}

如果有仔細看範例,你可以看到這一句:

1
defer db.Close()

他的意思是說要確保它可以正確的被關閉。

學會連接資料庫了,那要怎麼操作呢?我們來看一個簡單的例子,這邊是官方的範例:

1
2
3
4
5
stmtIns, err := db.Prepare("INSERT INTO squareNum VALUES( ?, ? )") // ? = placeholder
if err != nil {
  panic(err.Error()) // proper error handling instead of panic in your app
}
defer stmtIns.Close() // Close the statement when we leave main() / the program terminates

這邊使用 Prepare 來使用 sql 指令,將值傳給 stmtIns ,操作簡單直覺!

以上,這篇先簡單介紹 Golang,待之後專案使用到時,再來繼續深入研究!


Licensed under CC BY-NC-SA 4.0
comments powered by Disqus