Goはシンプルさと明確さを重視しており、他の言語だと当たり前にあるような機能がなかったりします。そこで普段はSwiftを書いている僕が、Goを学んでびっくりしたことを紹介します。
筆者はGoを学び始めて1ヶ月くらいの初心者です。そのため本記事には間違いが含まれている可能性が多分にあります。また本記事で言語の良し悪しを論じるつもりはありません。
列挙型がない!
Goには列挙型がなく、同様の機能をconst
とiota
を使って実装します。
type Day int
const (
Sunday Day = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
前述した通りGoは、シンプルさを重視している言語です。そのためenum
のように列挙型を定義する構文がなくても、他の機能で同様のことができるため列挙型がありません。
通常列挙型は要素を定義する以外にも、機能を追加できます。例えばSwiftであれば、要素の他にプロパティやメソッドを持つことができます。こうした機能は便利である反面、コードの複雑化につながります。なのでシンプルさを追求しているGoでは導入されていないわけです。
例外処理がない!
Goではエラーをthrowするのはでなく、戻り値として返すというアプローチを採用しています。そのため関数を呼び出す側がerrorがnilかどうかを判別する必要があります。
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の場合
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の場合
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の場合
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を学んで驚いたポイントをまとめると、次のような設計思想が垣間見えました:
-
シンプルさの追求:
多機能よりも最低限の機能で問題を解決する方針。 -
明確さの重視:
コードの読みやすさやエラーの追いやすさを優先。 -
軽量かつ効率的な並列処理:
実用的で分かりやすい並列処理の仕組みを提供。
Goは一見すると冗長に見える部分もありますが、その背景には「予測しやすく扱いやすいコード」を目指す明確な哲学があります。