😊

RubyエンジニアのためのGo言語入門 〜5つの重要な違いを理解する〜

2024/12/28に公開

はじめに

Rubyしか触れてこなかったエンジニアがGoを学ぶと、「そもそもこんな考え方自体がない」「Rubyの感覚とはまったく違う……」と感じる場面がいくつもあります。

ぼく自身もその1人です。

そこでこの記事では、自分自身が感じたRubyとGoの違いをまとめます。

この記事のゴール

  • RubyとGoの違いがなんとなくわかる

想定読者

  • Rubyしか経験がないけどGo言語のキャッチアップをしたいエンジニア

1. クラスが存在しない

Rubyではクラスを使ってオブジェクト指向プログラミングを行います。

class User
  attr_reader :name, :age
  
  def initialize(name:, age:)
    @name = name
    @age = age
  end
  
  def greet
    "私は#{@name}です。#{@age}歳です。"
  end
end

user = User.new(name: "太郎", age: 25)
puts user.greet

クラスを使うのは当然のように思えますが、実はGoにはクラスという概念が存在しません。

代わりに何を使うのか?

以下のように構造体(struct)と関数の組み合わせでクラスに近しい表現をします。

// 構造体の定義
// Ruby でいう User クラスに相当
type User struct {
    Name string
    Age  int
}

// メソッドの追加
// Ruby でいうインスタンスメソッドに相当
func (u User) Greet() string {
    return fmt.Sprintf("私は%sです。%d歳です。", u.Name, u.Age)
}

func main() {
    // インスタンス作成
    // Ruby でいう User.new に相当
    user := User{Name: "太郎", Age: 25}
    fmt.Println(user.Greet())
}

継承はどうする?

Goには継承の概念もありません。

代わりに構造体の埋め込み(コンポジションという)を使います。

type Animal struct {
    Name string
}

func (a Animal) Speak() string {
    return "..."
}

// 継承ではなく構造体の埋め込み
type Dog struct {
    Animal  // 埋め込み
    Breed string
}

func main() {
    dog := Dog{
        Animal: Animal{Name: "ポチ"},
        Breed: "柴犬",
    }
    // Animal のメソッドが使える
    fmt.Println(dog.Speak())
}

あくまでも継承ではなくコンポジションによって「継承っぽい」実装をしている状態です。

なぜ継承ではなくコンポジションなのか?

継承は複雑な階層構造を生み出してしまいがちです。

また継承だと親クラスの実装に依存してしまうため、変更が難しくなってしまうケースもあります。

その点、コンポジションであれば必要な機能だけを組み合わせられますし、親子関係を発生させたりしません。

親クラスの振る舞いを考慮しなくて良いため、テストも書きやすくなります。

Goはシンプルな言語仕様を設計思想としているため、複雑さをもたらす継承ではなくコンポジションを採用しているようです。

2. エラー処理が全く異なる

Rubyではbeginrescueなどのキーワードで例外処理を行います。

def divide(a, b)
  raise "ゼロ除算エラー" if b.zero?
  a / b
rescue => e
  puts "エラーが発生: #{e.message}"
end

一方、Goではエラーを戻り値として返します。

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("ゼロ除算エラー")
    }
    return a / b, nil
}

func main() {
    result, err := divide(10, 0)
    // 戻り値としてエラーが返されているか確認
    if err != nil {
        fmt.Println("エラーが発生:", err)
        return
    }
    fmt.Println(result)
}

そのため、Goでは以下のようなパターンが頻出します。

if err != nil {
    return err
}

冗長で面倒に感じられるかもしれませんが、ここは見慣れる必要があるでしょう。

エラーを戻り値として返す記法の利点

begin-rescueのような例外処理では、エラーの発生箇所から処理が大きく飛んでしまい、コードの流れを追うのが難しいです。

またパフォーマンスの面でも利点があります。

例外処理では通常スタックトレースが生成されますが、それなりのオーバーヘッドが伴います。

その点、Goのエラー処理は通常の関数呼び出しとして扱われるため、より効率的に動作します。

こういった点はむしろメリットなのかなと思いました。

3. ポインタの概念が必要

ポインタは、メモリ上の値の場所(アドレス)を指し示す値です。

Rubyではエンジニアがポインタを扱わなくて良いよう言語レベルで抽象化されているため、意識する必要がありません。

しかしGoではメモリ効率や値の更新のために重要な役割を果たします。

// User構造体の値を更新する例

// *User はUserのポインタ型を表す
// Rubyには存在しない概念だが、値の参照を扱うために必要
func updateName(user *User) {
    user.Name = "Bob"  // ポインタを通じて元の値を更新
}

func main() {
    // &を付けることでポインタを取得
    user := &User{Name: "Alice"}
    updateName(user)
    fmt.Println(user.Name)  // => "Bob"
}

特に構造体のメソッドを定義する際、値を更新したい場合はポインタレシーバを使う必要があります。

// 構造体の定義
type User struct {
    Name string
    Age  int
}

// 値レシーバの場合(Rubyのような挙動とは異なる)
// 構造体のコピーが作られるため元の値は更新されない
func (u User) SetName1(name string) {
    u.Name = name
}

// ポインタレシーバの場合(Rubyのような挙動に近い)
// 元の値を直接参照するため値が更新される
func (u *User) SetName2(name string) {
    u.Name = name
}

なぜGoでポインタが必要か?

大きなデータ構造を扱う場合、値をそのままコピーするのではなく、ポインタを使って参照することでメモリ使用量を抑えることができます。

また、ポインタを使うことで、上のサンプルコードのように関数やメソッドから元の値を直接更新できるようになります。

4. インターフェースの考え方が異なる

Rubyではダックタイピングによって事実上のインターフェースを実現します。

# ダックタイピングの例
class Dog
  def speak
    "わんわん"
  end
end

class Cat
  def speak
    "にゃーん"
  end
end

def make_sound(animal)
  # speakメソッドを持っていれば何でもOK
  puts animal.speak
end

make_sound(Dog.new)  # => わんわん
make_sound(Cat.new)  # => にゃーん

ただしRubyは動的型付け言語なので、インターフェースを満たしているかどうかは実行時にしかわかりません。

一方、Goでは明示的なインターフェースを定義します。

// インターフェースの定義
type Animal interface {
    Speak() string
}

type Dog struct {}

// Dogに対してSpeakメソッドを実装
func (d Dog) Speak() string {
    return "わんわん"
}

type Cat struct {}

// Catに対してSpeakメソッドを実装
func (c Cat) Speak() string {
    return "にゃーん"
}

func makeSound(a Animal) {
    fmt.Println(a.Speak())
}

func main() {
    makeSound(Dog{})  // => わんわん
    makeSound(Cat{})  // => にゃーん
}

Goは静的型付け言語なので、定義したインターフェースに対して抜け・漏れがあるとコンパイルエラーが発生します。

5. 並行処理の積極的な活用

RubyにはGIL(Global Interpreter Lock)という仕組みがあり、たとえマルチスレッドでもCPUバウンドの並列処理は制限されます。(I/O処理の並列実行は可能)

一方でGoはgoroutineというシンプルかつ安全に並行処理を実現する機能が提供されています。

func heavyTask(id int) {
    time.Sleep(time.Second)
    fmt.Printf("タスク%dが完了\n", id)
}

func main() {
    for i := 1; i <= 1000; i++ {
        // go キーワードを付けることで並行実行されます
        go heavyTask(i)  // 1000個のgoroutineを起動
    }
    // この待機は簡易的な例
    // 実際のアプリケーションでは sync.WaitGroup などを用いる
    time.Sleep(2 * time.Second)
}

この記事では並行処理の詳細には言及しませんが、channelによる通信やmutexを用いた排他制御など考慮すべき点がいくつかあるため、重点的に学習する必要があります。

まとめ:Ruby脳を切り替えるポイント

  1. クラス継承で考えない
    • 継承ベースの設計はGoでは避ける
    • 代わりに構造体とインターフェースを活用
  2. 例外処理に頼らない
    • エラーは戻り値として明示する
    • if err != nilとの付き合いを覚悟
  3. メモリと向き合う
    • ポインタの概念を理解する
    • 値のコピーとメモリ効率を意識する
  4. 並行処理の積極的な活用
    • マルチスレッド、goroutinechannelmutexなど重点的に学習する

Goはシンプルな仕様の言語だと言われますが、Rubyとは根本的に異なる考え方や設計思想を持っていると感じます。

特に上記のポイントは、Rubyエンジニアが最初にぶつかる壁となりやすい部分かなと思い、ピックアップしました。

これらのパラダイムの違いを理解することで、Goのキャッチアップが加速するはずです。

最後に、この記事はぼく自身Rubyの経験しかない状況からGoの勉強をしつつまとめた内容です。

記事の内容に誤りがありましたら、ご指摘いただけると幸いです。

Discussion