Np-Urのデータ分析教室

オーブンソースデータなどWeb上から入手できるデータを用いて、RとPython両方使って分析した結果を書いていきます

PythonユーザーのためのGo入門。PythonとGoの文法の違い早見表をまとめました【Goへの移行チートシート】

PythonユーザーのためのGo入門
PythonユーザーのためのGo入門

この記事は、Zennで(無料)販売しているこちらの本のまとめ(チートシート的役割)として、執筆しました。

zenn.dev

本記事では説明は省略しているので、ぜひ書籍もあわせてご覧ください。

PythonからGoへの移行で必要な知識を実践的なコード例とともに解説します。単純な対応表ではなく、なぜその書き方になるのか、どんな場面で使うのかまでまとめます。

この記事で分かること

  • Pythonの書き方をGoで実現する具体的な方法
  • 型システムの違いと実践的な対処法
  • エラーハンドリングの考え方の違い
  • 並行処理の書き方とパフォーマンス向上のコツ

変数宣言:型を意識した書き方

Pythonの動的な変数宣言

Pythonでは変数宣言が非常にシンプルです。

# Python
x = 10
name = "Alice"
numbers = [1, 2, 3]

Goの静的型付けによる宣言

Goでは型を意識した宣言が必要です。ただし、型推論により簡潔に書けます。

// Go - 型推論を使った短縮記法
x := 10          // int型として推論
name := "Alice"  // string型として推論
numbers := []int{1, 2, 3}  // int型のスライス

// 明示的な型指定
var x int = 10
var name string = "Alice"
var numbers []int = []int{1, 2, 3}

実践ポイント

  • 関数内では:=を使った短縮記法が一般的
  • パッケージレベル(グローバル)変数はvarで宣言
  • 型推論を活用してコードを簡潔に

複数変数の同時代入

Pythonの多重代入はGoでも同様に使えます:

# Python
x, y = 1, 2
a, b = b, a  # 値の交換
// Go
x, y := 1, 2
a, b = b, a  // 値の交換

条件分岐:ブロック構造の違い

Pythonのインデントベース

# Python
if age >= 18:
    print("成人です")
elif age >= 13:
    print("中高生です")
else:
    print("子供です")

Goの波括弧ベース

// Go
if age >= 18 {
    fmt.Println("成人です")
} else if age >= 13 {
    fmt.Println("中高生です")
} else {
    fmt.Println("子供です")
}

Goの特殊な条件分岐:初期化付きif文

Goならではの便利な機能として、if文内で変数を初期化できます:

// Go特有の書き方
if age := 30; age >= 18 {
    fmt.Println("成人です")
    // ageはこのifブロック内でのみ有効
}

// ファイル処理でよく使われるパターン
if file, err := os.Open("data.txt"); err == nil {
    defer file.Close()
    // ファイル処理
} else {
    fmt.Printf("ファイルを開けません: %v\n", err)
}

ループ:for文だけで全てを表現

Pythonの豊富なループ構文

# Python - 様々なループ
for i in range(10):
    print(i)

for item in items:
    print(item)

for i, item in enumerate(items):
    print(f"{i}: {item}")

while condition:
    # 処理
    pass

Goのfor文による統一的な表現

Goではfor文一つで全てのループパターンを表現します。

// Go - C言語スタイルのfor文
for i := 0; i < 10; i++ {
    fmt.Println(i)
}

// range句を使った要素の反復
for _, item := range items {
    fmt.Println(item)
}

// インデックスと要素の両方を取得
for i, item := range items {
    fmt.Printf("%d: %v\n", i, item)
}

// while文相当
for condition {
    // 処理
}

// 無限ループ
for {
    // 処理
    if shouldBreak {
        break
    }
}

実践ポイント

  • _は使わない値を明示的に捨てる記号
  • rangeはスライス、マップ、チャネルで使用可能
  • breakcontinueはPythonと同じように使える

関数定義:戻り値と型の明示

Pythonの関数定義

# Python
def greet(name: str) -> str:
    return f"Hello, {name}!"

def calculate(x: int, y: int) -> tuple[int, int]:
    return x + y, x * y

def process_data(**kwargs):
    # 可変キーワード引数
    pass

Goの関数定義

// Go - 基本的な関数
func greet(name string) string {
    return fmt.Sprintf("Hello, %s!", name)
}

// 複数の戻り値
func calculate(x, y int) (int, int) {
    return x + y, x * y
}

// 名前付き戻り値
func divide(a, b float64) (result float64, err error) {
    if b == 0 {
        err = fmt.Errorf("division by zero")
        return
    }
    result = a / b
    return
}

// 可変引数
func sum(numbers ...int) int {
    total := 0
    for _, n := range numbers {
        total += n
    }
    return total
}

実践ポイント

  • Goでは複数戻り値が一般的(特にエラーハンドリング)
  • 可変引数は...を使用
  • 名前付き戻り値で可読性を向上

データ型:静的型付けの活用

基本型の扱い

# Python - 動的型付け
number = 42
price = 99.99
name = "商品A"
is_available = True
// Go - 型を意識した宣言
var number int = 42
var price float64 = 99.99
var name string = "商品A"
var isAvailable bool = true

// 型推論での簡潔な書き方
number := 42
price := 99.99
name := "商品A"
isAvailable := true

リストとスライス

# Python - リスト
fruits = ["apple", "banana", "orange"]
fruits.append("grape")
fruits.extend(["kiwi", "mango"])
sub_fruits = fruits[1:3]
// Go - スライス
fruits := []string{"apple", "banana", "orange"}
fruits = append(fruits, "grape")
fruits = append(fruits, []string{"kiwi", "mango"}...)
subFruits := fruits[1:3]

// スライスの容量を指定した初期化
fruits := make([]string, 0, 10)  // 長さ0、容量10

実践ポイント

  • スライスは動的配列として使用
  • appendは新しいスライスを返すため、代入が必要
  • makeでメモリ効率の良い初期化が可能

辞書とマップ

# Python - 辞書
user = {"name": "Alice", "age": 30}
if "name" in user:
    print(user["name"])

for key, value in user.items():
    print(f"{key}: {value}")
// Go - マップ
user := map[string]interface{}{
    "name": "Alice",
    "age":  30,
}

// 値の存在確認
if name, ok := user["name"]; ok {
    fmt.Println(name)
}

// 反復処理
for key, value := range user {
    fmt.Printf("%s: %v\n", key, value)
}

// 型安全なマップ(推奨)
userInfo := map[string]string{
    "name":  "Alice",
    "email": "alice@example.com",
}

文字列操作:stringsパッケージの活用

Pythonのメソッドチェーン

# Python
text = "  Hello, World!  "
result = text.strip().upper().replace("HELLO", "HI")
words = result.split(", ")
joined = " | ".join(words)

Goの関数ベース操作

// Go
import "strings"

text := "  Hello, World!  "
trimmed := strings.TrimSpace(text)
upper := strings.ToUpper(trimmed)
replaced := strings.ReplaceAll(upper, "HELLO", "HI")
words := strings.Split(replaced, ", ")
joined := strings.Join(words, " | ")

// チェーン風に書くための工夫
func processText(text string) string {
    text = strings.TrimSpace(text)
    text = strings.ToUpper(text)
    text = strings.ReplaceAll(text, "HELLO", "HI")
    return text
}

実践ポイント

  • stringsパッケージの関数は純粋関数(元の文字列を変更しない)
  • 文字列は不変(immutable)
  • パフォーマンスが重要な場合はstrings.Builderを使用

エラーハンドリング:例外ではなく戻り値

Pythonの例外処理

# Python
try:
    with open("data.txt", "r") as file:
        content = file.read()
        result = process_data(content)
    print("処理完了")
except FileNotFoundError:
    print("ファイルが見つかりません")
except ValueError as e:
    print(f"データエラー: {e}")
finally:
    print("クリーンアップ")

Goのエラー戻り値

// Go
func processFile() error {
    file, err := os.Open("data.txt")
    if err != nil {
        return fmt.Errorf("ファイルを開けません: %w", err)
    }
    defer file.Close() // finallyの代わり

    content, err := io.ReadAll(file)
    if err != nil {
        return fmt.Errorf("読み込みエラー: %w", err)
    }

    result, err := processData(string(content))
    if err != nil {
        return fmt.Errorf("データ処理エラー: %w", err)
    }

    fmt.Println("処理完了")
    return nil
}

// 呼び出し側
if err := processFile(); err != nil {
    log.Printf("エラー: %v", err)
}

実践ポイント

  • エラーは戻り値として明示的に処理
  • deferで確実なリソース管理
  • エラーのラップ(%w)で詳細情報を保持

構造体:クラスの代替としての使い方

Pythonのクラス

# Python
class User:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age
        self._email = ""  # プライベート風

    def greet(self) -> str:
        return f"Hello, I'm {self.name}"

    @property
    def email(self) -> str:
        return self._email

    @email.setter
    def email(self, value: str):
        if "@" in value:
            self._email = value
        else:
            raise ValueError("Invalid email")

user = User("Alice", 30)
print(user.greet())

Goの構造体とメソッド

// Go
type User struct {
    Name  string
    Age   int
    email string // 小文字で非公開
}

// コンストラクタ関数
func NewUser(name string, age int) *User {
    return &User{
        Name: name,
        Age:  age,
    }
}

// メソッド
func (u *User) Greet() string {
    return fmt.Sprintf("Hello, I'm %s", u.Name)
}

// ゲッター
func (u *User) Email() string {
    return u.email
}

// セッター
func (u *User) SetEmail(email string) error {
    if !strings.Contains(email, "@") {
        return fmt.Errorf("invalid email")
    }
    u.email = email
    return nil
}

// 使用例
user := NewUser("Alice", 30)
if err := user.SetEmail("alice@example.com"); err != nil {
    log.Printf("Error: %v", err)
}
fmt.Println(user.Greet())

実践ポイント

  • 構造体の大文字フィールドは公開、小文字は非公開
  • レシーバーはポインタ型(*User)を使うのが一般的
  • コンストラクタ関数で初期化ロジックを管理

並行処理:ゴルーチンとチャネル

Pythonのスレッド処理

# Python
import threading
import queue
import time

def worker(q, results):
    while True:
        item = q.get()
        if item is None:
            break
        # 重い処理
        time.sleep(1)
        results.put(f"Processed {item}")
        q.task_done()

q = queue.Queue()
results = queue.Queue()
threads = []

for i in range(3):
    t = threading.Thread(target=worker, args=(q, results))
    t.start()
    threads.append(t)

# タスクを追加
for i in range(10):
    q.put(f"task-{i}")

q.join()

# スレッド終了
for i in range(3):
    q.put(None)
for t in threads:
    t.join()

Goのゴルーチンとチャネル

// Go
import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, tasks <-chan string, results chan<- string, wg *sync.WaitGroup) {
    defer wg.Done()
    for task := range tasks {
        // 重い処理
        time.Sleep(time.Second)
        results <- fmt.Sprintf("Worker %d processed %s", id, task)
    }
}

func processData() {
    tasks := make(chan string, 10)
    results := make(chan string, 10)
    var wg sync.WaitGroup

    // 3つのワーカーを起動
    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go worker(i, tasks, results, &wg)
    }

    // タスクを送信
    go func() {
        for i := 0; i < 10; i++ {
            tasks <- fmt.Sprintf("task-%d", i)
        }
        close(tasks) // チャネルを閉じる
    }()

    // 結果を受信
    go func() {
        wg.Wait()
        close(results)
    }()

    // 結果を出力
    for result := range results {
        fmt.Println(result)
    }
}

実践ポイント

  • ゴルーチンは軽量スレッド(数万個でも起動可能)
  • チャネルで安全なデータ共有
  • sync.WaitGroupで完了待機
  • チャネルのクローズで処理終了を通知

まとめ:PythonからGoへの移行のコツ

この記事では、PythonからGoへの移行で重要なポイントを実践的なコード例とともに解説しました。

重要な考え方の違い

  1. 型システム: 動的型付けから静的型付けへ
  2. エラーハンドリング: 例外から戻り値ベースへ
  3. 並行処理: スレッドからゴルーチンとチャネルへ
  4. メモリ管理: deferによる確実なクリーンアップ処理

PythonとGoそれぞれの良さを理解し、適切な場面で使い分けられるのが良さそうですね。