Go言語

気になることのメモ。

Goにおける列挙型(enum)の表現

Go言語には、多くの他のプログラミング言語に存在する列挙型(enum)はありません。しかし、Goでenumのような機能を実現する方法があります。

基本的な列挙型の表現

以下のコードは、色を表現するColor型を定義する例です。

// 後で触れますが、これは推奨されない方法です。

type Color int

const (
    Red Color = iota
    Blue
    Yellow
)

この例では、Colorという新しい型を定義し、int型をベースとしています。これは、iotaが整数を生成するためです。この定義により、Redは0、Blueは1、Yellowは2という値を持つことになります。

多くの場合、このような数値から具体的な文字列を取得したいという要求があります。そのため、以下のようにStringメソッドを定義します。

func (c Color) String() string {
    switch c {
    case Red:
        return "Red"
    case Blue:
        return "Blue"
    case Yellow:
        return "Yellow"
    default:
        return "Unknown"
    }
}

注意点と改善方法

上記の方法は、一見すると問題なく動作するように見えますが、実は問題点があります。

Goにおけるint型のゼロ値は0です。このため、Color型のゼロ値も0となります。上記の例では、ゼロ値がRedに対応してしまうため、何も指定しない場合のデフォルトがRedとなってしまいます。

この問題を解決するためには、ゼロ値をUnknownとして定義するか、iota+1で数え始める方法があります。

type Color int

const (
    UnknownColor = iota  // ゼロ値をUnknownとして定義
    Red
    Blue
    Yellow
)

const (
    Red Color = iota + 1  // 1から数え始める
    Blue
    Yellow
)
  • 個人的には、uintで定義するのが好きです。 uint型を使用することで、負の値を持たない列挙型を作成できる。

追加情報

  • Stringerインターフェース: Stringメソッドは、Goの組み込みインターフェースであるStringerインターフェースを実装しています。これにより、fmt.Printlnなどの関数でColor型の変数を直接出力すると、定義した文字列が表示されます。
  • 列挙型の拡張: 列挙型を拡張する場合、新しい値を追加する際にはリストの最後に追加することで、既存のコードの動作を変更しないようにすることが推奨されます。
  • 列挙型の比較: 列挙型の値は、基本型(この場合はint)として比較することができます。これにより、switch文やif文での条件分岐が容易になります。
  • 列挙型の安全性: Goでは、列挙型のようなものを使うことで、特定の値のセットのみを受け入れるように型を制限することができます。これにより、不正な値が設定されるのを防ぐことができます。。

参考文献

  1. Goのenumのようなものの定義方法
  2. Goのiotaの完全理解

Goのstructとメソッド

GoはJavaやC++のような伝統的なオブジェクト指向言語とは異なり、classの概念がない。その代わり、structを使ってデータ構造を定義し、それに関連するメソッドを結びつけることができる。

  • 継承の欠如: Goには継承の概念がない。しかし、structの中に別のstructを組み込むことで、継承のような機能を模倣することができる。これにより、多重継承の問題を回避することができる。

  • インターフェースの暗黙性: Goのインターフェースは他の言語とは異なり、型がインターフェースのメソッドを持っていれば、明示的に宣言することなくそのインターフェースを実装するとみなされる。これはコードの柔軟性と再利用性を高める。

  • ポリモーフィズム: Goはインターフェースを利用してポリモーフィズムをサポートしている。これにより、異なる型でも同じインターフェースを実装していれば、同じように扱うことができる。

  • カプセル化: Goでは名前の先頭が大文字であればそれは公開され、小文字であれば非公開となる。これにより、パッケージの境界をもとにしたカプセル化が可能となる。

Goのインターフェースの詳細

基本的なインターフェースの定義: SpeakerというインターフェースはSpeakメソッドを持つことを要求する。

type Speaker interface {
    Speak() string
}

インターフェースの実装: DogCatはそれぞれSpeakメソッドを実装している。Goでは、これだけでSpeakerインターフェースを実装しているとみなされる。

type Dog struct{}

func (d Dog) Speak() string {
    return "Woof!"
}

type Cat struct{}

func (c Cat) Speak() string {
    return "Meow!"
}

インターフェースの利用: Introduce関数はSpeakerインターフェースを引数として受け取り、そのSpeakメソッドを呼び出す。

func Introduce(s Speaker) {
    fmt.Println("The animal says:", s.Speak())
}

func main() {
    d := Dog{}
    c := Cat{}

    Introduce(d)  // 出力: The animal says: Woof!
    Introduce(c)  // 出力: The animal says: Meow!
}

カプセル化の詳細

カプセル化は、データとそのデータを操作するメソッドを一つの単位にまとめ、外部からの不正なアクセスや変更を防ぐオブジェクト指向の核心的な概念である。Goでは、公開と非公開の2つの可視性のみをサポートしており、これによりシンプルかつ効果的なカプセル化が実現されている。

Goにおけるカプセル化の利点

Goのカプセル化のアプローチは以下の利点を持つ。

  1. シンプルさ: Goのカプセル化は名前の先頭文字の大文字・小文字で実現。private, protected, publicのような修飾子は不要。
  2. 明確なパッケージの境界: Goのカプセル化はパッケージレベルで行われる。どの部分が外部に公開されているか、どの部分が非公開かが明確。
  3. データの整合性の維持: 非公開のフィールドやメソッドを通じてのみデータにアクセスできる。データの整合性を維持しやすい。

Goにおけるカプセル化の実践

Goでカプセル化を実践する方法は以下の通り。

  1. ゲッターとセッター: 非公開のフィールドに対して、ゲッター(getter)やセッター(setter)を提供。フィールドの直接的なアクセスを制限しつつ、必要な操作を許可。

    type Circle struct {
        radius float64
    }
    
    func (c *Circle) Radius() float64 {
        return c.radius
    }
    
    func (c *Circle) SetRadius(r float64) {
        if r > 0 {
            c.radius = r
        }
    }
    
  2. 非公開のヘルパーメソッド: 複雑な操作や計算を非公開のヘルパーメソッドで定義。公開メソッドからそれを呼び出すことで、内部のロジックを隠蔽。

  3. インターフェースを利用したカプセル化: 具体的な型の詳細を隠蔽し、インターフェースを公開。実装の詳細を隠すことができる。

Goは独自の方法でカプセル化をサポートし、データの保護と整合性を維持することができる。

「カプセル化についてまとめたけど、Goでこれをやるメリットはあまり感じられないので使える時に使おうという感想」

ゼロ幅のメモリ割り当て

ゼロ幅のメモリ割り当ての用途は主に、パフォーマンスの最適化やメモリ使用量の削減にある。

type key struct{}

var p key = struct{}{}

特別なメソッド

Go言語では、特定のインターフェイスを実装することにより、型に対して追加の振る舞いを定義することができます。その中でも特によく使われるのが、StringメソッドとGoStringメソッドです。

Stringメソッド

Stringメソッドは、fmt.Stringerインターフェイスを実装することによって定義されます。このインターフェイスは、String() stringというシグネチャを持つ単一のメソッドを要求します。このメソッドを実装することにより、その型のインスタンスを文字列として表現する方法をカスタマイズできます。fmtパッケージの関数(例えばfmt.Printlnfmt.Printfなど)を通じてオブジェクトが出力される際に、このStringメソッドが自動的に呼び出され、その返り値が使用されます。

type Person struct {
    Name string
    Age  int
}

func (p Person) String() string {
    return fmt.Sprintf("%s (%d years)", p.Name, p.Age)
}

上記の例では、Person型にStringメソッドを実装しています。このメソッドは、Personインスタンスを"Name (Age years)"の形式の文字列に変換します。

GoStringメソッド

GoStringメソッドは、fmt.GoStringerインターフェイスを実装することで定義されます。このインターフェイスはGoString() stringというメソッドを要求し、このメソッドはオブジェクトのGo言語における文字列表現を提供することを目的としています。fmtパッケージの%#vのようなフォーマット指定子を用いた出力時に、このGoStringメソッドがあれば、それが呼び出されます。

func (p Person) GoString() string {
    return fmt.Sprintf("Person{Name: %q, Age: %d}", p.Name, p.Age)
}

上記の例では、Person型にGoStringメソッドを実装しています。このメソッドは、PersonインスタンスをPerson{Name: "Name", Age: Age}の形式のGoのコードとして表現する文字列に変換します。これはデバッグ時にオブジェクトの状態を明確に理解するのに役立ちます。

まとめ

  • Stringメソッドは、オブジェクトの文字列表現をカスタマイズするために使用され、fmt.Stringerインターフェイスを通じて定義されます。
  • GoStringメソッドは、オブジェクトのGo言語における文字列表現を提供し、fmt.GoStringerインターフェイスを通じて定義されます。
  • これらのメソッドを適切に実装することで、ログ出力やデバッグ情報の表示をより有効にすることができます。