跳至主要内容

[Go] Basic Syntax

這篇文章包含一些 Golang 的基本語法

Why Go?

Go 是 Google 內部的三位員工在 2007 年開發的語言,主要具有以下特點 :

  • 強型別的靜態語言
  • 編譯型語言
  • 簡潔的語法
  • 高效的併發支持以及 garbage collection
  • 簡單的依賴管理

Setting up Go in VScode

  1. 安裝 Go
  2. 設定環境變數 GOROOTGOPATH
  3. 安裝 Go extension for VScode
  4. 設定 auto formatter
.vscode/settings.json
{
"[go]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "golang.go"
}
}

Hello World

main.go
package main

import "fmt"

func main() {
fmt.Println("Hello, World!")
}
go run main.go

Package

Go 的 package 是用來組織程式碼的單位,每個程式檔案都需要屬於某個 package

引用的路徑可以是相對路徑或是絕對路徑,絕對路徑是指從 $GOPATH/src 開始的路徑,而相對路徑則是指相對於當前檔案的路徑,我們還可以取別名來引用包

Go 會從 package main 開始執行程式,所以每個程式只能有一個 package main

package foo

import (
"go/types"
"golang.org/x/syscall"
"errors"
xerrors "golang.org/x/errors"
_ "os/signal"
)

如果我們要將 package 匯出,則需要將 package 名稱開頭大寫

為引入的 package 取的別名,可以使用 _ 來忽略,這樣就會執行 package 的 init 函數並且避免未引用的錯誤

Variables

Basic Type

主要的基本型態有以下幾種 :

bool

int, int8, int16, int32, int64
uint, uint8, uint16, uint32, uint64, uintptr
float32, float64
complex64, complex128

string
byte
rune
  • int : 整數,根據系統位元數不同而有不同的範圍
  • uint : 正整數,根據系統位元數不同而有不同的範圍
  • uintptr : 用來儲存指標的整數,一般情況下不建議使用,常與 unsafe package 一起使用
  • complex64 : 由兩個 float32 組成的複數 (實部與虛部)
  • string : 字串
  • byte : 類似 uint8,用來儲存字元
  • rune : 類似 int32,用來儲存 Unicode 字元

Constants & Variables

與大部分的程式語言相同,Go 也可以用 constvar 來宣告常數與變數

func main() {
const (
name = "val"
PI float64 = 3.1415926
)
var (
age = 18
)

fmt.Println(name, age, PI)
}

如果已經有賦值的話,可以省略型態

func main() {
var name = "val"
var age = 18

fmt.Println(name, age)
}

如果在函數裡面,也可以使用 := 來宣告變數

func main() {
name := "val"
age := 18

fmt.Println(name, age)
}

在函數外面,則不可以使用 := 來宣告變數

name := "val"
// syntax error: non-declaration statement outside function body

func main() {
fmt.Println(name)
}

Zero Value

Go 的變數在宣告時,如果沒有賦值的話,會有一個 zero value,以下是一些基本型態的 zero value :

int : 0
float : 0.0
bool : false
string : ""
pointer : nil
slice : nil

Type Conversion

Go 的型態轉換需要顯式轉換,不支援隱式轉換

func main() {
var a int = 10
var b float64 = float64(a)
fmt.Println(b)
}

Pointer

Go 的指標與 C 類似,可以透過 & 取得變數的記憶體位置,透過 * 取得指標指向的值,差別在於不支援指標運算

func main() {
a := 10
b := &a
fmt.Println(a, b)
// 10 0xc0000b6010

*b = 20
fmt.Println(a, *b)
// 20 20
}

Struct

Struct 是一種自定義的複合型態,可以用來組織不同的變數

type person struct {
age int
sex bool
}

func main() {
p := person{age: 32, sex: true}
fmt.Println(p.age)
fmt.Printf("%v\n", p)
fmt.Printf("%+v\n", p)
fmt.Printf("%#v\n", p)
}

// 32
// {32 true}
// {age:32 sex:true}
// main.person{age:32, sex:true}

可以用指標來讀取 struct 的值,Go 允許我們直接使用 p2.age 來取得值,而不需要使用 (*p2).age

func main() {
p := person{age: 32, sex: true}
fmt.Println(p.age)

p2 := &p
fmt.Println(p2.age)
}

Array & Slice

Go 同樣支持陣列,但陣列的大小是固定的,如果需要動態增加大小可以使用 slice

func main() {
var a [2]string
a[0] = "Hello"
a[1] = "World"
fmt.Println(a[0], a[1])
fmt.Println(a)

primes := [6]int{2, 3, 5, 7, 11, 13}
fmt.Println(primes)
}
func main() {
primes := [6]int{2, 3, 5, 7, 11, 13}

var s []int = primes[1:4]
fmt.Println(s)
}
// [3 5 7]

需要注意的是,slice 是 reference type,所以當我們對 slice 做修改時,原本的陣列也會被修改

func main() {
names := [4]string{
"John",
"Paul",
"George",
"Ringo",
}
fmt.Println(names)

a := names[0:2]
b := names[1:3]
fmt.Println(a, b)

b[0] = "XXX"
fmt.Println(a, b)
fmt.Println(names)
}

// [John Paul George Ringo]
// [John Paul] [Paul George]
// [John XXX] [XXX George]
// [John XXX George Ringo]

slice 有三個屬性,分別是指向陣列的指標、長度 (len)、容量 (cap),其中長度是指 slice 的長度,容量是指分配給 slice 的記憶體大小

如果 slice 的 len > cap,則會重新分配記憶體,並且將原本的值複製到新的 slice 中

func main() {
s := []int{2, 3, 5, 7, 11, 13}
printSlice(s)

// Slice the slice to give it zero length.
s = s[:0]
printSlice(s)

// Extend its length.
s = s[:4]
printSlice(s)

// Drop its first two values.
s = s[2:]
printSlice(s)
}

func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

// len=6 cap=6 [2 3 5 7 11 13]
// len=0 cap=6 []
// len=4 cap=6 [2 3 5 7]
// len=2 cap=4 [5 7]

可以用 make 來建立 slice,make 會建立一個 zero value 的 slice,並且初始化長度與容量

func main() {
a := make([]int, 5)
printSlice("a", a)

b := make([]int, 0, 5)
printSlice("b", b)

c := b[:2]
printSlice("c", c)

d := c[2:5]
printSlice("d", d)
}

func printSlice(s string, x []int) {
fmt.Printf("%s len=%d cap=%d %v\n", s, len(x), cap(x), x)
}

// a len=5 cap=5 [0 0 0 0 0]
// b len=0 cap=5 []
// c len=2 cap=5 [0 0]
// d len=3 cap=3 [0 0 0]

可以建立多維 slice

func main() {
board := [][]string{
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
}

board[0][0] = "X"
board[2][2] = "O"
board[1][2] = "X"
board[1][0] = "O"
board[0][2] = "X"

for i := 0; i < len(board); i++ {
fmt.Printf("%s\n", strings.Join(board[i], " "))
}
}

可以在 slice 中加入元素

func main() {
var s []int
printSlice(s)

s = append(s, 0)
printSlice(s)

s = append(s, 1)
printSlice(s)

s = append(s, 2, 3, 4)
printSlice(s)
}

func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

// len=0 cap=0 []
// len=1 cap=1 [0]
// len=2 cap=2 [0 1]
// len=5 cap=6 [0 1 2 3 4]

可以使用 range 來迭代 slice,如果只需要某個值 (i, v),可以使用 _ 來忽略

func main() {
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
}

// 2**0 = 1
// 2**1 = 2
// ...

String

Go 的字串是不可變的,但可以透過 slice 來取得部分字串

func main() {
s := "Hello, World!"
fmt.Println(s)
// Hello, World!

fmt.Println(s[0])
// 72

fmt.Println(string(s[0]))
// H

fmt.Println(len(s))
// 13

fmt.Println(s[0:5])
// Hello

fmt.Println(s[7:])
// World!

fmt.Println(s[:5])
// Hello

fmt.Println("Hello, " + "World!")
// Hello, World!
}

如果需要修改字串,可以透過 []byte 或是 []rune 來修改

func main() {
s := "Hello, World!"
b := []byte(s)
b[0] = 'h'
fmt.Println(string(b))
// hello, World!

r := []rune(s)
r[0] = 'h'
fmt.Println(string(r))
// hello, World!
}

Map

Go 的 map 是一種 key-value 的 hash table

func main() {
m := map[string]int{"foo": 42}
fmt.Println(m)
// map[foo:42]

fmt.Println(m["foo"])
// 42

m["foo"] = 100
fmt.Println(m)
// map[foo:100]

for k, v := range m {
fmt.Println(k, v)
}
// foo 100

elem, ok := m["foo"]
fmt.Println(elem, ok)
// 100 true

delete(m, "foo")
fmt.Println(m)
// map[]
}

Control Flow

If-Else

在 Go 的 if-else 中,我們可以在條件式前面加上一個 statement,這個 statement 的範圍只在 if-else 內

func main() {
if n := 42; n%3 == 0 {
fmt.Println("n is divisible by 3")
} else if n%3 == 1 {
fmt.Println("n divided by 3 leaves a remainder of 1")
} else {
fmt.Println("n divided by 3 leaves a remainder of 2")
}
}

Switch

Go 同樣也有 switch 的語法,會從上到下執行,直到遇到匹配的 case,如果沒有匹配的 case,則會執行 default

func main() {
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
fmt.Printf("%s.\n", os)
}
}

除此之外,Go 有一個 keyword fallthrough 可以用來強制執行下一個 case,但這個 keyword 會影響可讀性,不建議使用

func main() {
switch i := 1; i {
case 1:
fmt.Println("one")
fallthrough
case 2:
fmt.Println("two")
case 3:
fmt.Println("three")
}
}
// one
// two

For Loop

與 C 類似,Go 也有 for 迴圈,參數同樣為初始化、條件、遞增

func main() {
for i := 0; i < 5; i++ {
fmt.Println(i)
}
}

如果想要有類似 while 的效果,可以省略初始化與遞增

func main() {
i := 0
for i < 5 {
fmt.Println(i)
i++
}
}

Function

以下是一個基本的函數,吃兩個整數,回傳一個整數

func add(a int, b int) int {
return a + b
}

如果參數的類型相同,可以簡寫成

func add(a, b int) int {
return a + b
}

如果有多個回傳值,可以透過 () 包起來

func foo(a int, b int) (int, error) {
if b == 0 {
return 0, errors.New("b cannot be 0")
}
d := a / b

return d, nil
}

func main() {
d, err := foo(10, 0)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(d)
}
}

也有 naked return 的方式,可以直接回傳變數,但影響可讀性因此不建議使用

func foo(a, b int) (d int, err error) {
if b == 0 {
err = errors.New("b cannot be 0")
return
}
d = a / b
return
}

除此之外,function 也可以是一種型態,可以當作參數傳遞

func add(a, b int) int {
return a + b
}

func sub(a, b int) int {
return a - b
}

func calc(a, b int, f func(int, int) int) int {
return f(a, b)
}

func main() {
fmt.Println(calc(10, 5, add))
fmt.Println(calc(10, 5, sub))
}

同時,在 Go 的 function 也具有閉包的特性

所謂的閉包 (closure) 是指內部函式能夠取得函式外部的變數,並且記住這個變數

在下面的例子中,匿名函式 adder 會記住 sum 的值,並且在每次呼叫時累加

func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}

func main() {
pos, neg := adder(), adder()
for i := 0; i < 5; i++ {
fmt.Println(
pos(i),
neg(-2*i),
)
}
}

// 0 0
// 1 -2
// 3 -6
// 6 -12
// 10 -20

Break & Continue

在 Go 中,breakcontinue 的用法與其他語言相同

func main() {
for i := 0; i < 5; i++ {
if i == 3 {
break
}
fmt.Println(i)
}

for i := 0; i < 5; i++ {
if i == 3 {
continue
}
fmt.Println(i)
}
}

Defer

defer 會在函數結束時執行,通常用來釋放資源,採用 LIFO 的方式執行

func foo() {
defer fmt.Println("1")
fmt.Println("2")
defer fmt.Println("3")
}

func main() {
foo()
}
// 2 3 1

Interface & Method

在 Go 中沒有 class 的概念,但可以透過 struct 與 method 來實現類似的功能

type Vertex struct {
X, Y float64
}

func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs())
}

也可以定義在指標上,這樣可以修改 struct 的值

func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}

func main() {
v := Vertex{3, 4}
v.Scale(10)
fmt.Println(v.Abs())
}

// 50

或者也可以使用另一種寫法

func Scale(v *Vertex, f float64) {
v.X = v.X * f
v.Y = v.Y * f
}

func main() {
v := Vertex{3, 4}
Scale(&v, 10)
fmt.Println(v.Abs())
}

在 Go 中,interface 是一種定義行為的方式,可以將 interface 定義為一個類型,然後在其他地方實現這個 interface

比較特別的是,Go 的 interface 並不需要顯式宣告,只要 struct 實作了 interface 的所有方法,就可以當作該 interface 使用

type Shape interface {
Area() float64
}

type Circle struct {
Radius float64
}

func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}

type Rectangle struct {
Width, Height float64
}

func (r Rectangle) Area() float64 {
return r.Width * r.Height
}

func main() {
var s Shape

c := Circle{Radius: 5}
r := Rectangle{Width: 3, Height: 4}

s = c
fmt.Println("Circle Area:", s.Area())

s = r
fmt.Println("Rectangle Area:", s.Area())
}

除此之外,Go 還有一種空 interface,可以接受任何型態

像是 json 格式就可以處理成 map[string]interface{},這樣就可以接受任何型態的 json

func describe(i interface{}) {
fmt.Printf("(%v, %T)\n", i, i)
}

func main() {
var i interface{}
describe(i)

i = 42
describe(i)

i = "hello"
describe(i)
}

當遇到空的 interface 時,可以使用 type assertion 或是 type switch 來取得原本的型態

func main() {
var i interface{} = "hello"

s := i.(string)
fmt.Println(s)

s, ok := i.(string)
fmt.Println(s, ok)

f, ok := i.(float64)
fmt.Println(f, ok)

f = i.(float64)
fmt.Println(f)
// panic: interface conversion: interface {} is string, not float64
}
func do(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}

Concurrency

Goroutine

在 Go 中,我們可以透過 go 關鍵字來啟動一個 goroutine,goroutine 是一種輕量級的執行緒,可以讓我們更有效率地使用 CPU

func foo() {
for i := 0; i < 5; i++ {
fmt.Println("foo:", i)
time.Sleep(1 * time.Second)
}
}

func main() {
go foo()

for i := 0; i < 5; i++ {
fmt.Println("main:", i)
time.Sleep(1 * time.Second)
}
}

Channel

channel 的本質是一個環型的 queue,可以用來傳遞和接收資料

主要分成兩種,一種是 unbuffered channel,另一種是 buffered channel,如果要使用 buffered channel,要在建立 channel 時指定 buffer 的大小,否則就是 unbuffered channel

我們可以先透過 make 來建立一個 unbuffered channel

func main() {
ch := make(chan int)
go func() {
ch <- 42
fmt.Println("sent")
}()

time.Sleep(1 * time.Second)

go func() {
val := <-ch
fmt.Println("received", val)
}()

time.Sleep(1 * time.Second)
}

// received 42
// sent

透過上面的範例我們可以觀察到,在沒有 buffer 的情況下,傳送端會等到有人接收資料後才能繼續執行

但如果我們有 buffer 的話,則可以先將資料放入 buffer,這樣傳送端就不需要等待接收端

func main() {
ch := make(chan int, 1)
go func() {
ch <- 42
fmt.Println("sent")
}()

time.Sleep(1 * time.Second)

go func() {
val := <-ch
fmt.Println("received", val)
}()

time.Sleep(1 * time.Second)
}

// sent
// received 42

我們可以利用 channel 來實現一個簡單的 producer-consumer 模型,記得需要使用 close 來表示不能再往 channel 寫入資料

func main() {
ch := make(chan int, 20)
wg := sync.WaitGroup{}
wg.Add(2)

go func() {
defer wg.Done()
for i := 0; i < 5; i++ {
ch <- i
}
}()
go func() {
defer wg.Done()
for i := 0; i < 5; i++ {
ch <- i
}
}()

wg2 := sync.WaitGroup{}
wg2.Add(1)
go func() {
sum := 0
for {
i, ok := <-ch
// 如果 channel 已經關閉 (調用 close),且 channel 內沒有資料的話,ok 才會是 false
if !ok {
break
}
sum += i
}
fmt.Println("sum:", sum)
wg2.Done()
}()

wg.Wait()
close(ch)
wg2.Wait()
}

Select

select 可以用來監聽多個 channel,當其中一個 channel 有值時,就會執行該 case

func producer(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}

func consumer(ch1, ch2 chan int) {
for {
select {
case i, ok := <-ch1:
if !ok {
ch1 = nil
} else {
fmt.Println("ch1:", i)
}
case i, ok := <-ch2:
if !ok {
ch2 = nil
} else {
fmt.Println("ch2:", i)
}
}
if ch1 == nil && ch2 == nil {
break
}
}
}

func main() {
ch1 := make(chan int, 5)
ch2 := make(chan int, 5)
go producer(ch1)
go producer(ch2)
consumer(ch1, ch2)
}

Mutex

在 Go 中,我們可以使用 sync.Mutex 來實現鎖,避免多個 goroutine 同時修改變數

type SafeCounter struct {
v map[string]int
mux sync.Mutex
}

func (c *SafeCounter) Inc(key string) {
c.mux.Lock()
c.v[key]++
c.mux.Unlock()
}

func (c *SafeCounter) Value(key string) int {
c.mux.Lock()
defer c.mux.Unlock()
return c.v[key]
}

func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 30; i++ {
go c.Inc("key")
}

time.Sleep(time.Second)
fmt.Println(c.Value("key"))
}

Error Handling

Go 的 error handling 是基於 error 這個 interface

type error interface {
Error() string
}

也因此,任何實作了 Error() 方法的型態都可以當作 error 來使用

type CustomErr struct {
err error
}
func (c CustomErr) Error() string {
return fmt.Sprintf("err: %v", c.err)
}

也可以在函數中設定多個回傳值,其中最後一個回傳值通常是 error

func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}

func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)
}

在 Go 中也有一些內建的處理方式,像是 panicrecover

panic 會中斷程式,並且執行 defer 的函數,如果沒有 recover 的話,程式就會結束

func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered:", r)
}
}()

panic("panic")
}

Generics

Go 支援泛型,內建一些 type constraints,像是 anycomparable

func Print[T any](s []T) {
for _, v := range s {
fmt.Println(v)
}
}

func main() {
Print([]int{1, 2, 3})
Print([]string{"a", "b", "c"})
}

同時,在 type 中也可以使用 type constraints

type Pair[T comparable] struct {
First, Second T
}

func main() {
p := Pair{First: 1, Second: 2}
fmt.Println(p)
}

Go command

以下會列出 Go 常用的指令

go run main.go # 編譯並執行程式
go build main.go # 編譯程式,並產生 .exe 檔
go build -o <output_name> main.go # 指定輸出檔名和位置

go get <package_path> # 下載 package
go get -u <package_path> # 更新 package
go get -u # 更新所有 package
go get <package_path>@none # 移除 package


go install main.go # 編譯並安裝程式

go mod init <module_name> # 初始化 module,建立 go.mod 來管理 package
go mod tidy # 移除並新增缺少的 package

go test # 測試程式
go test -v # 顯示詳細資訊
go test -run <test_name> # 執行特定的 test
go test -cover # 顯示 coverage

go fmt # 格式化程式碼

Reference