SwiftエンジニアがGoを学んでびっくりしたこと

Goはシンプルさと明確さを重視しており、他の言語だと当たり前にあるような機能がなかったりします。そこで普段はSwiftを書いている僕が、Goを学んでびっくりしたことを紹介します。

:::note warn
筆者はGoを学び始めて1ヶ月くらいの初心者です。そのため本記事には間違いが含まれている可能性が多分にあります。また本記事で言語の良し悪しを論じるつもりはありません。
:::

## 列挙型がない！
Goには列挙型がなく、同様の機能を`const`と`iota`を使って実装します。
```Go
type Day int

const (
    Sunday Day = iota
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)
```
前述した通りGoは、シンプルさを重視している言語です。そのため`enum`のように列挙型を定義する構文がなくても、他の機能で同様のことができるため列挙型がありません。

通常列挙型は要素を定義する以外にも、機能を追加できます。例えばSwiftであれば、要素の他にプロパティやメソッドを持つことができます。こうした機能は便利である反面、コードの複雑化につながります。なのでシンプルさを追求しているGoでは導入されていないわけです。

## 例外処理がない！
Goではエラーをthrowするのはでなく、戻り値として返すというアプローチを採用しています。そのため関数を呼び出す側がerrorがnilかどうかを判別する必要があります。
### Swiftの場合
```Swift
enum SimpleError: Error {
    case hogeError
}

func performTask(shouldFail: Bool) throws -> String {
    if shouldFail {
        throw SimpleError.somethingWentWrong
    }
    return "Task succeeded"
}

func handleErrorExample() {
    do {
        let result = try performTask(shouldFail: true)
        print(result)
    } catch {
        print("Error occurred: \(error)")
    }
}

handleErrorExample()
```
Swiftの場合はエラーを返す関数に`throws`をつけ、エラーの場合はエラーをthrowします。
関数を使う側が`do-catch`でthrows関数を呼び出し、エラーに場合はcatch内の処理が実行されます。

### Goの場合
```Go
package main

import (
	"errors"
	"fmt"
)

func performTask(shouldFail bool) (string, error) {
	if shouldFail {
		return "", errors.New("something went wrong")
	}
	return "Task succeeded", nil
}

func main() {
	result, err := performTask(true)
	if err != nil {
		fmt.Println("Error occurred:", err)
		return
	}
	fmt.Println(result)
}
```
Goの場合はエラーを返す可能性のある関数は、エラーを戻り値に含めます。関数を使う側は、戻り値のエラーがnilかどうかを判別します。

このアプローチを採用した背景も、Goがシンプルさを追求した言語だからです。

`do-catch`などの例外処理を用いると、例えばエラーが関数を伝播していく場合のように、実際にエラーが起こった箇所とそれをハンドリングする箇所が離れてしまいます。そのためどこでエラーが起こったのかを追いにくくなってしまいます。

逆にGoでは関数を使った側が、すぐにエラーチェックをしなければいけないため、どこでエラーが発生したのかが明確です。一見冗長に見えますが、明確さとシンプルさを追求した書き方というわけです。

## 並列処理が簡単！
Goでは`goroutine`と`channel`を使って、並列処理が簡単に記述できます。
### Swiftの場合
```Swift
import Foundation

func processItem(_ item: Int) async -> String {
    try? await Task.sleep(nanoseconds: 1 * 1_000_000_000)
    return "Processed \(item)"
}

func processAllItems() async {
    let items = [1, 2, 3, 4, 5]
    
    let results = await withTaskGroup(of: String.self) { group in
        for item in items {
            group.addTask {
                await processItem(item)
            }
        }
        
        var results: [String] = []
        for await result in group {
            results.append(result)
        }
        
        return results
    }
    
    print("All results: \(results)")
}

Task {
    await processAllItems()
}
```
Swiftで並列処理を記述するには`TaskGroup`を用います。このタスクグループに実行したい処理を追加していくことで、並列処理を実行できます。

### Goの場合
```Go
package main

import (
	"fmt"
	"time"
)

func processItem(item int, ch chan string) {
	time.Sleep(1 * time.Second)
	ch <- fmt.Sprintf("Processed %d", item)
}

func processAllItems() {
	items := []int{1, 2, 3, 4, 5}
	ch := make(chan string, len(items)) // channelを作成

	for _, item := range items {
		go processItem(item, ch)
	}

	var results []string
	for i := 0; i < len(items); i++ {
		results = append(results, <-ch)
	}

	fmt.Println("All results: ", results)
}

func main() {
  processAllItems()
}
```
Goでは`go`をつけて関数を呼び出すだけで、簡単に並列処理を実行できます。またデータの送受信も`channel`を使うことで簡単に実装できます。

またgoroutineは軽量なので、少ないメモリで多くのgoroutineを実行することができます。またchannelは排他的にデータをやり取りするので、データ競合が起こる心配もありません。

## まとめ
Goを学んで驚いたポイントをまとめると、次のような設計思想が垣間見えました：

1. **シンプルさの追求:**
多機能よりも最低限の機能で問題を解決する方針。


2. **明確さの重視:**
コードの読みやすさやエラーの追いやすさを優先。

3. **軽量かつ効率的な並列処理:**
実用的で分かりやすい並列処理の仕組みを提供。

Goは一見すると冗長に見える部分もありますが、その背景には「予測しやすく扱いやすいコード」を目指す明確な哲学があります。

## 参考書籍
- https://amzn.asia/d/5EuZVLF