[Go] Basic Syntax
這篇文章包含一些 Golang 的基本語法
Why Go?
Go 是 Google 內部的三位員工在 2007 年開發的語言,主要具有以下特點 :
- 強型別的靜態語言
- 編譯型語言
- 簡潔的語法
- 高效的併發支持以及 garbage collection
- 簡單的依賴管理
Setting up Go in VScode
- 安裝 Go
- 設定環境變數
GOROOT
與GOPATH
- 安裝 Go extension for VScode
- 設定 auto formatter
{
"[go]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "golang.go"
}
}
Hello World
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 也可以用 const
跟 var
來宣告常數與變數
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 中,break
與 continue
的用法與其他語言相同
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 中也有一些內建的處理方式,像是 panic
與 recover
panic 會中斷程式,並且執行 defer 的函數,如果沒有 recover 的話,程式就會結束
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered:", r)
}
}()
panic("panic")
}
Generics
Go 支援泛型,內建一些 type constraints,像是 any
跟 comparable
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"})
}