學習軌道
/
Go
Go
/
練習
/
千層麵大師
千層麵大師

千層麵大師

學習練習

簡介

函式能讓你將程式碼組成可重複使用的單位。 它由func關鍵字、函式的名稱,以及以小括號包住、以逗號分隔的零個或多個參數和型別所組成。

函式參數

所有參數都必須明確標註型別;參數沒有型別推斷。 參數也沒有預設值,所以所有函式參數都是必填的。

import "fmt"

// No parameters
func PrintHello() {
    fmt.Println("Hello")
}

// Two parameters
func PrintGreetingName(greeting string, name string) {
  fmt.Println(greeting + " " + name)
}

型別相同的參數可以一起宣告,後面接著單一的型別宣告。

import "fmt"

func PrintGreetingName(greeting, name string) {
  fmt.Println(greeting + " " + name)
}

參數與引數

讓我們快速說明兩個經常被混為一談的詞:parameters和arguments。 函式參數是定義在函式簽章中的名稱,例如上方PrintGreetingName函式中的greeting和name。 函式引數則是我們呼叫函式時,實際傳給函式參數的具體值。 例如,在下面的範例中,"Hello"和"Katrina"就是傳給greeting和name參數的引數:

PrintGreetingName("Hello", "Katrina")

回傳值

函式參數之後接著零個或多個回傳值,這些回傳值也必須明確標註型別。 單一的回傳值不加括號,多個回傳值則以括號包住。 函式使用return關鍵字將值回傳給呼叫端的程式碼。 一個函式中可以有多個return敘述。 函式的執行會在遇到其中一個return敘述時立刻結束。 如果要從函式回傳多個值,值與值之間以逗號分隔。

func Hello(name string) string {
  return "Hello " + name
}

func HelloAndGoodbye(name string) (string, string) {
  return "Hello " + name, "Goodbye " + name
}

呼叫函式

呼叫函式時,要指定函式名稱,並在小括號中為函式的每一個參數傳入引數。

import "fmt"

// No parameters, no return value
func PrintHello() {
    fmt.Println("Hello")
}
// Called like this:
PrintHello()

// One parameter, one return value
func Hello(name string) string {
  return "Hello " + name
}
// Called like this:
greeting := Hello("Dave")

// Multiple parameters, multiple return values
func SumAndMultiply(a, b int) (int, int) {
    return a+b, a*b
}
// Called like this:
aplusb, atimesb := SumAndMultiply(a, b)

具名回傳值與裸回傳

除了參數之外,回傳值也可以選擇性地命名。 如果使用了具名回傳值,那麼不帶引數的return敘述就會回傳這些值。 這就是所謂的「裸回傳」。

func SumAndMultiplyThenMinus(a, b, c int) (sum, mult int) {
    sum, mult = a+b, a*b
    sum -= c
    mult -= c
    return
}

傳值與傳參考

釐清傳值與傳參考的概念也很重要。

首先,讓我們釐清傳值。 在下面的範例中,當我們把變數val傳給函式MultiplyByTwo時,傳入的是val的複本。 因此,newVal會是更新後的值4,但原始的變數val仍然是2。 在背後,Go 基本上會複製原始值,因此只有這個複本(也就是v)會被函式修改。

val := 2
func MultiplyByTwo(v int) int {
    v = v * 2
    return v
}
newVal := MultiplyByTwo(val)
// newval is 4, val is still 2 because only a copy of its value was passed into the function

嚴格來說,在 Go 中所有引數都是傳值,也就是會複製提供給函式的值或資料。 但如果你不想複製傳入函式的資料,並且想在函式中修改這些資料, 就應該改用pointers作為引數,也就是傳參考。

指標

我們使用pointer來達成傳參考。 將pointer引數傳入函式之後, 我們就能修改傳入函式的底層資料,而不只是對資料的複本進行操作。

目前你只需要知道,指標型別可以透過函式簽章中型別前面的*來辨認。

func HandlePointers(x, y *int) {
    // Some logic to handle integer values referenced by pointers x and y
}

如果pointer的概念讓你感到困惑,別擔心。 我們在課程大綱後面的內容有專門的章節,可以幫助你精通指標。

例外

請注意,slices和maps是上述規則的例外。 當我們把slice或map當作引數傳入函式時,即使型別中沒有明確的*,它們也會被視為指標型別。 這表示如果我們把 slice 或 map 傳入函式並修改其底層資料, 這些變更會反映在原始的 slice 或 map 上。

說明

在這個練習中,你會再寫一些程式碼,內容跟準備和烹調你最愛食譜裡的極品千層麵有關。

這次有四項任務。 第一項跟烹調本身有關,另外三項則是關於完美的準備工作。

1. 估算準備時間

為了下一份要做的千層麵,你想確保預留了足夠的時間,好讓你能好好享受烹調的過程。 你已經規劃好千層麵要有哪些層了。 現在你想據此估算準備工作會花多久時間。

請實作一個函式 PreparationTime,它接受以 []string 表示的層的切片,以及每層的平均準備時間(以分鐘為單位)作為 int。 這個函式應該根據層數,回傳預估的總準備時間,型別為 int。 Go 的函式沒有預設值。 如果傳入的平均準備時間是 0(int 的預設初始值),就應該採用預設值 2。

layers := []string{"sauce", "noodles", "sauce", "meat", "mozzarella", "noodles"}
PreparationTime(layers, 3)
// => 18
PreparationTime(layers, 0)
// => 12

2. 計算所需的麵條和醬汁份量

除了預留時間之外,你也想確保有足夠的醬汁和麵條,好做出你夢想中的千層麵。 千層麵中每一層麵條層,需要 50 公克的麵條。 每一層醬汁層,則需要 0.2 公升的醬汁。

定義函式 Quantities,它接受以 []string 表示的層的切片作為參數。 這個函式接著會算出做這道料理所需的麵條和醬汁數量。 結果應該以兩個值回傳:noodles 為 int,sauce 為 float64。

Quantities([]string{"sauce", "noodles", "sauce", "meat", "mozzarella", "noodles"})
// => 100, 0.4

3. 加入祕密食材

前陣子你去拜訪一位朋友,並在他家吃了千層麵。 那千層麵非常棒,而且有股特別的風味。 朋友把食材陣列寄給你,並告訴你陣列上的最後一項,就是讓那頓飯如此特別的「祕密食材」。 現在你也想把这个祕密食材加進自己的食譜裡。

撰寫一個函式 AddSecretIngredient,它接受兩個型別為 []string 的食材切片作為參數。 第一個參數是朋友寄給你的陣列,第二個是你自己食譜的食材陣列。 你的食材陣列中,最後一個元素一定是 "?"。 這個函式應該把它替換成你朋友陣列中的最後一項。 注意: AddSecretIngredient 不會回傳任何東西,你應該直接修改你自己的食材陣列。 朋友的食材陣列則不該被修改。 另外,由於 slice 是以指標的形式傳入函式,傳進 AddSecretIngredient 的兩個 []string 引數若有變動,會直接生效。

friendsList := []string{"noodles", "sauce", "mozzarella", "kampot pepper"}
myList := []string{"noodles", "meat", "sauce", "mozzarella","?"}

AddSecretIngredient(friendsList, myList)
// myList => []string{"noodles", "meat", "sauce", "mozzarella", "kampot pepper"}

4. 調整食譜份量

你的食譜上列出的份量,只夠做出兩人份的千層麵。 因為你下次想煮給更多人吃,所以你想算出不同份數所需的份量。

請實作一個函式 ScaleRecipe,它接受兩個參數。

  • 一個 float64 的切片,內含兩人份所需的份量。
  • 你想做的份數。

這個函式應該回傳一個 float64 切片,內含所需份數的份量。 不過,你想保留原本的食譜。 這表示 quantities 這個引數不應該在這個函式中被修改。

quantities := []float64{ 1.2, 3.6, 10.5 }
scaledQuantities := ScaleRecipe(quantities, 4)
// => []float64{ 2.4, 7.2, 21 }
透過 GitHub 編輯 連結會在新視窗或分頁中開啟
Go Exercism

準備好開始 千層麵大師 了嗎?

註冊 Exercism,透過 34 個概念166 個練習 和真人引導來學習並精通 Go,全部免費。