iT邦幫忙

2023 iThome 鐵人賽

DAY 15
0
Modern Web

就是個Go,我也可以啦!GOGO系列 第 15

2023鐵人賽Day 15 Go 之道:從 if 的快樂路徑到 for range 的安全遊走

  • 分享至 

  • xImage
  •  

在寫go時,你會發現go希望的是 “一件事情僅有一種做法的理念” 只保留了 for 這一種迴圈結構,去掉了 C 語言中的 while 和 do-while 迴圈結構,並且填平了 C 語言中 switch 分支結構中每個 case 語句都要以 break 收尾的“坑”,Go語言做了很多事,所以我們要學習的是在Go語言你應該要用什麼語言思維和慣用手法來書寫

if 的快樂路徑

下面有兩段,可以看看你覺得看哪一段比較好

func someThing() error {
  if errorCondition1{
    // 錯誤邏輯
    return error1  
  }
  // 成功邏輯
  
  if errorCondition2{
    // 錯誤邏輯
    return error2  
  }
  // 成功邏輯
  return nil
}

以及下面這段

func someThing() error{
  if successCondition1 {
    // 成功邏輯
    if successCondition2{
      //成功邏輯
      return nil
    }else {
      // 錯誤邏輯
      return error2
    }
  }else {
    // 錯誤邏輯
    return error1
  }
}

應該可以很容易地看出哪一段好哪一段壞
好的是第一段,因為他遵循了快樂路徑

快樂路徑(happy path)

  • 單一分支的控制結構,通常指的是使用 if 而不是 if-else 或 switch
  • 當你遇到錯誤或異常情況時,你應該在 if 裡面直接返回或處理錯誤,而不是進入正常的邏輯分支,這也被稱為「早期返回」(early return)
  • 把正常邏輯的程式碼寫得儘量不縮進,這樣它會靠左邊,如果你先處理錯誤情況並且早期返回,那麼你的正常邏輯不需要放在 else 塊中,因此不需要額外的縮進
  • 函數執行到最後一行,那麼它代表一種成功的狀態,因為所有可能的錯誤情況都已經被提前捕捉和返回了
func DoSomething(input int) (result string, err error) {
    if input <= 0 {
        return "", errors.New("input must be positive")
    }
    
    // 正常邏輯靠左
    result = fmt.Sprintf("Processed number: %d", input)
    return result, nil
}

為什麼我們會在go中特別強調這個概念呢?

因為在很多程式語言中(例如 Java、Python 或 C#),錯誤或異常通常是透過“異常拋出”來處理的。當某個異常情況發生時,程式會拋出一個異常,這會中斷目前的程式流程,然後該異常會被一個專門設計來捕捉異常的代碼區塊捕獲,像是在javascript就會使用.catch來接錯誤

在這個 Go 函數中,當嘗試進行除以零的操作時,它不會拋出異常,而是返回一個錯誤物件。當沒有錯誤時,返回 nil 作為 error 類型的值

所以你使用這個函數的時候,你會這樣處理錯誤

result, err := devide(10,0)

if err!= nil {
  fmt.Println("Error", err)
  return
}

fmt.Println("Result:", result)

Go鼓勵明確地處理錯誤,而不是依賴拋出和捕捉異常的機制

迭代值的捕獲,for range 中使用閉包時,循環變數的陷阱

當我們使用 for range 時

var a := [...]int{1,2,3,4,5}
for i, v := range a {
  ...
}

可以用顯示等價轉換看得更清楚

{
  var a := [...]int{1,2,3,4,5}
  {
    i, v := 0
    for i,v = range a {
      ...
    }
  }
}

i, v := 0 是在for外面被宣告的,可以說for裡面都是用同個變數
這店可以先記著

閉包

閉包是一個函數,它能夠“捕獲”或“記住”其外部範疇中的變量

var a = [...]int{1, 2, 3, 4, 5}
funcs := []func(){}
for i, v := range a {
    funcs = append(funcs, func() {
        fmt.Println(i, v)
    })
}
for _, fn := range funcs {
    fn()
}

第一個循環是用於創建閉包函數的,而第二個循環是用於調用這些函數的
第一循環

for i, v := range a {
    funcs = append(funcs, func() {
        fmt.Println(i, v)
    })
}

定義並存儲閉包到 funcs 切片中,但這些閉包此時還沒有被執行。它們只是被存儲起來,等待後續的調用

第二循環

for _, fn := range funcs {
    fn()
}

在這個循環中,每一個之前存儲的閉包都被調用一次,此時 fmt.Println(i, v) 才會執行,輸出捕獲的 i 和 v 值

但你執行完時會發現

❯ go run main.go
4 5
4 5
4 5
4 5
4 5

由於所有的閉包共享相同的循環變量,所以它們捕獲的是這些變量的最終值,因此輸出將會是5次相同的最終值,而不是順序的1到5

解方

for i, v := range a {
  i, v := i, v //新的local variable
  funcs = append(funcs, func() {
    fmt.Println(i, v)
  })
}

每次迭代創建了新的 i 和 v 變量,這樣閉包就會捕獲這些新變量的值,而不是外部循環變數的值

參與range迭代的是 “副本”

在 Go 中,for range 迭代的是被迭代對象的副本,而不是原始對象本身

slice := []int{1, 2, 3}

for _, v := range slice {
    v *= 10  // 這個操作不會改變 slice 中的元素
}

fmt.Println(slice)  // 輸出: [1, 2, 3]

儘管我們在循環內部修改了 v,但這不會更改 slice 的原始數據。這是因為 v 是迭代時 slice 的一個元素的副本

解方

對於切片:使用索引来修改元素

slice := []int{1, 2, 3}
for i, v := range slice {
    slice[i] = v * 10
}
fmt.Println(slice)  // 輸出: [10, 20, 30]

  • i 是當前元素的索引的副本。
  • v 是當前元素值的副本。
  • slice 本身在這個上下文中並不是副本。

使用索引 i 來直接修改 slice 的元素。即使 v 是元素的副本,但我們並不使用它來進行修改,而是使用索引 i

對於映射:使用鍵 (key) 來修改值

m := map[int]string{
    1: "one",
    2: "two",
    3: "three",
}
for k, v := range m {
    m[k] = v + " changed"
}
fmt.Println(m)  // 輸出: map[1:one changed 2:two changed 3:three changed]

  • k 和 v 是映射的鍵和值的副本
  • 對 k 或 v 進行任何修改都不會影響映射 m 本身
  • m 在這個上下文中不是副本,可以直接通過鍵 k 來修改映射中的值,如 m[k] = v + " changed"
    使用鍵 k 來直接修改映射 m 中的值

總結:當你使用 for range 迭代映射時,鍵和值都是副本,但你仍然可以透過鍵直接修改映射的原始數據。


上一篇
2023鐵人賽Day 14 Go 哪兒?變數作用域在哪裡
下一篇
2023鐵人賽Day 16 Go x 探討方法本質及RECEIVER
系列文
就是個Go,我也可以啦!GOGO30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言