[Notes] Go/Golang 基本語法 Part2

Haren Lin
14 min readJul 8, 2021

--

Cute Gophers

指標 Pointer

Go 保留了 C 的指標,因此如果要在程式裡動態配置記憶體的話,都要使用指標來存放位置。指標變數初始值是 nil,表示尚未指派記憶體位址。

/* code segment 1 */
var ptr *string // declare a pointer named ptr pointing to string
fmt.Printf("%p \n", ptr) // print out: 0x0 (尚未指派記憶體位置)
if ptr == nil {
fmt.Println("ptr is nil")
} // print out: "ptr is nil"
/* code segment 2 */
str := "Haren Lin"
var ptr *string = &str
fmt.Printf("%p \n", ptr) // print out: 0xc000096220
fmt.Printf("%s \n", *ptr) // print out: Haren Lin

最後,因為安全性的考量,Go 不支援指標算術運算!

Why is there no pointer arithmetic?

Safety. Without pointer arithmetic it’s possible to create a language that can never derive an illegal address that succeeds incorrectly. Compiler and hardware technology have advanced to the point where a loop using array indices can be as efficient as a loop using pointer arithmetic. Also, the lack of pointer arithmetic can simplify the implementation of the garbage collector.

講到指標了,不免俗的也要跟學 C 時一樣來玩玩 swap function。

func swap(a, b *int) {
var temp int = *a
*a = *b
*b = temp
}
func main() {
a, b := 54, 39
swap(&a, &b) // pass the address into swap func
println(a, b) // print out: 39, 54
}

接下來,就是動態記憶體配置的部分。跟 C++ 很像,使用 new 來新增記憶體配置。

ptr := new(string) // dynamic allocation
*ptr = "Haren Lin"
fmt.Printf("%s \n", *ptr) // print out: "Haren Lin"

回覆一下記憶:如何動態宣告 Slice?用 make

// 動態宣告一個slice x 裡面存放 float64 的數值,且長度為5,容量為10
x := make([]float64, 5, 10)

映射 Map

這東西在 Python 是 dict,在 C++ 裡面是 unordered_map。怎麼建立一個簡單的 map,並給予儲存值呢?map 後面的中括號裡面,要放的是 key 的資料型態,後面接著放的是 value 的資料型態。

// General Declaration
var ageOfHeros = map[string]int{"IronMan": 30, "Dr.Strange": 45}
// Dynamic Allocation
var ageOfHeros := make(map[string]int)
ageOfHeros["IronMan"] = 30
ageOfHeros["Dr.Strange"] = 45

然而,新增一個新的對應關係到 map 很直觀,那要怎麼刪除呢?要使用 delete(map_name, key_name) 函式,其中,刪除不存在的 key 不會造成錯誤。此外,要怎麼知道現在 map 中有多少個 key-value pair 呢?這時使用的是 len(map_name) 函式。

接著,假設我們現在想存取 map 裡面的內容,但是他不存在呢?我們應該要先確認他是否存在於這個 map 當中,請看下方範例。name 其實對應到 的就是 map[key] 的 value,而 ok 是返回它有沒有值,True / False。

最後來玩玩很酷的東西,map 裡面存放函式。(Go 中函式也是一種 Data Type,可當變數使用)

結構 Struct

在進到 struct 之前,可以先了解一下到底 Go 算不算物件導向的語言?簡單講,他少了繼承,所以只能稱之為物件導向風格。

Is Go an object-oriented language?

Yes and no. Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy. The concept of “interface” in Go provides a different approach that we believe is easy to use and in some ways more general. There are also ways to embed types in other types to provide something analogous — but not identical — to subclassing. Moreover, methods in Go are more general than in C++ or Java: they can be defined for any sort of data, even built-in types such as plain, “unboxed” integers. They are not restricted to structs (classes). Also, the lack of a type hierarchy makes “objects” in Go feel much more lightweight than in languages such as C++ or Java.

而在 Go 裡面,沒有 class,完全使用 struct,但它的功能遠比你想像強大很多。而其實一開始的宣告架構,與 C 裡面的 struct 很像。

在 OO 中,較常用 Reference 來傳遞 Object,以便可以確保現在操作的 Object 是同一個。在 Go,可以用指標來傳遞 struct address,達到傳 Reference 的效果。以下範例用結構在宣告時就用 &,表示回傳一個位址,用這個位址傳入函式操作。

此外,Go 也可以用 new(struct_name) 建立結構,效果和 &struct_name 一樣 。但用 new 不能同時初始化結構裡的欄位

方法 Method

概念就是 C++ Class Method。最特別的地方就是方法的定義和以往我們學過的語言都不一樣。C++ 方法的定義會寫在類別之中;而 Go 沒有類別,故方法不是寫在結構內,而是寫在外面,並且會透過 Receiver 來指定方法給一個結構型別。直接看例子,觀察 line10–12 的 Method 定義以及 line20 的使用。

想像一下之前學 OO 教授常舉的例子,二維平面中的點。

內嵌 Embedding

OOP 常會用 Inheritance 來共享父類別程式碼。然而,Go 沒有繼承的特性,但能用「組合 Compisition」的方式來共享程式碼。不僅如此,Go 還提供一種優於組合的語法特性,稱作內嵌 Embedding

以下範例能幫助了解所謂的組合,即 struct 再包 struct,透過這樣的方式共享 struct 資料或 method。

Go 的內嵌其實就是 Composition 的概念,只是更簡潔更強大。內嵌允許我們在 struct 內組合其他 struct 時,不需要定義欄位名稱,且能直接透過該 struct 叫用欄位或方法。實際上,內嵌的 struct 欄位還是有名稱,就 struct 本身的名稱(跟C++概念一樣)。我們將上面的範例改成使用內嵌,如下:

另外,也可以用具名的方法初始化:

var haren = &Member{
Person: &Person{"Haren Lin"}
TeamName: "ERS"
TeamID: 16
}

如果把上面的例子,再加入之前學的 Method:

補充:如果你的 struct 裡面含有不同 struct 但卻有相同名字的成員變數,那你在呼叫的時候就必須 specifically 指出你要 access 的是哪個 struct 中的變數,避免 Ambiguity。

介面 Interface

可以先看看以下這部影片,會有更好的觀念理解!說到底,概念就像是 C++ 裡面的 Polymorphism 實現。

(1) Interface 是 Type,定義為 Method Signature 的集合。
(2) Interface 的 Value 可是任何實作 Interface Method 的 Value。

介面是 OOP 抽象化的最大關鍵,通常會用於程式與模組之間的解藕。如何定義介面?其實語法很像結構,只改用 interface 關鍵字。一個介面可以由一個或多個 Method Signature 組成,能相容於實作所有 Method Signature 的結構 struct。以下方範例而言,定義了一個訊息發送者介面,所有實作 Send() 方法的結構都能相容於 MessageSender。

其中,interface 變數的型別會是包含 struct 的型別。因此,我們可以認定 interface 是一種特別變數,它的值和型別都是依據其包含的 struct 實體

此外,宣告的介面變數沒有指定實體的話,預設值是 nil,如果直接執行介面中的方法就會 Error。

var sender MessageSender
fmt.Println(sender) // print out: <nil>

介面也可以內嵌?我們知道 Go 沒有繼承,但是有提供內嵌來補足繼承這塊。不但 struct 有內嵌,interface 也可以使用內嵌,達到介面擴展介面的效果。

// 定義三個 interface 且 Publisher 裡面有 MessageSender & Logger (內嵌)type MessageSender interface {  
Send(content string)
}
type Logger interface {
Log(info string)
}
type Publisher interface {
MessageSender
Logger
Connect()
}
type EmailSender struct{}func (s *EmailSender) Send(content string) {}
func (s *EmailSender) Log(info string) {}
func (s *EmailSender) Connect() {}
func describe(i interface{}) {
fmt.Printf("(%v, %T)\n", i, i)
}
func main() {
var pub Publisher
pub = &EmailSender{}
describe(pub) // print out: (&{}, *main.EmailSender)
}

另外,有時候會需要將 interface 轉成原來的 struct 型別,Go 可使用型別斷言 Type Assertion 將 interface 轉成型別,或是另一個 interface。公式如下:

t := itfc.(T)
// itfc = 你現在手上的 interface
// T = Target struct 轉換後的目標型別
// t = 轉換後的目標變數。

下方例子會更好理解:

// interface definition
type MessageSender interface {
Send(content string)
}
// struct definition
type EmailSender struct{
Address string
}
func (s *EmailSender) Send(content string) {}
func main() {
var sender MessageSender
sender = &EmailSender{Address: "testing@gmail.com"}
emailSender := sender.(*EmailSender) // Type Assertion
fmt.Println(emailSender.Address) // print out: testing@gmail.com
}

但要注意,如果介面中的實體跟想要轉過去的型別搞混了,會跳出 panic 產生錯誤:

// interface definition
type MessageSender interface {
Send(content string)
}
// struct definition 1
type EmailSender struct{
Address string
}
func (s *EmailSender) Send(content string) {}
// struct definition 2
type SmsSender struct{
Mobile string
}
func (s *SmsSender) Send(content string) {}
func main() {
var sender MessageSender
sender = &EmailSender{Address: "testing@gmail.com"}
smsSender := sender.(*SmsSender) // Type Assertion
fmt.Println(smsSender.Mobile) // ERROR!!!
}

對於這種狀況,Go 提供另一種寫法可以避免這樣的錯誤:

smsSender, ok := sender.(*SmsSender)
if ok {
fmt.Println(smsSender.Mobile)
}

這樣一來,轉型失敗時 ok 的值是 False,且不會噴 Error。

最後,Type Switches,可用 switch-case 來分流 interface 的型別。透過判斷不同種的 type,執行相對應的事情。

switch v := itfc.(type) // type 是保留字,可拿出 interface value 的型別switch v := itfc.(type) {    
case int:
fmt.Printf() ...
case string:
....
default:
....
}

如果想更白話的了解 Interface 何時用到,可以參考一下文章:簡單講,透過 Interface 定義可以讓別人順利接上自己的東西,又不會把自己的套件寫死只支援少許的功能。

[補充] Sorting Interface:裡面有 Len, Swap, Less 這三個 Method Signature,我們要手動實現。另外,必須將 Array of Person 手動定義出一個 Data Type,才能操作。

參考資料 / Reference

1. A Tour of Go
2. 初學 Golang 30 天
3. Golang Crash Course
4. Learn Go Programming — Golang Tutorial for Beginners

This article will be updated at any time! Thanks for your reading. If you like the content, please click the “clap” button. You can also press the follow button to track new articles at any time. Feel free to contact me via LinkedIn or email.

--

--

Haren Lin
Haren Lin

Written by Haren Lin

MSWE @ UC Irvine | MSCS @ NTU GINM | B.S. @ NCCU CS x B.A. @ NCCU ECON | ex-SWE intern @ TrendMicro

No responses yet