😺

Goの基本概念だけを学習してみた

2024/06/16に公開

はじめに

Go は近年非常に人気があり、多くの企業や開発者が使用しています。この記事では、Go の基本的な概念について学び、そのメリットを探ります。

ポインタ

Go ではポインタを使用して変数のアドレスを参照できます。これにより、関数間でデータを効率的に共有することができます。

package main

import "fmt"

func main() {
    x := 5
    fmt.Println("Before:", x)
    increment(&x)
    fmt.Println("After:", x)
}

func increment(x *int) {
    *x = *x + 1
}

このコードでは、increment 関数に x のポインタを渡しています。increment 関数内で x の値を変更すると、main 関数内の x の値も変更されます。

ポインタのメリット

  1. 効率的なデータ共有: 大きなデータを関数に渡す際に、データ全体をコピーするのではなく、ポインタを渡すことで効率的にデータを共有できます。

  2. メモリ管理: ポインタを使用することで、メモリ管理が容易になります。

構造体とメソッド

Go では、構造体とメソッドを使用してデータとその操作をまとめることができます。これにより、オブジェクト指向プログラミングのようなスタイルでコードを書くことができます。

package main

import "fmt"

type Rectangle struct {
    Width  int
    Height int
}

func (r Rectangle) Area() int {
    return r.Width * r.Height
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    fmt.Println("Area:", rect.Area())
}

このコードでは、Rectangle 構造体とそのメソッド Area を定義しています。Area メソッドは Rectangle の面積を計算します。

構造体とメソッドのメリット

  1. データのカプセル化: データとその操作を一つの単位としてまとめることで、コードの可読性と保守性が向上します。

  2. 再利用性: 構造体とメソッドを定義することで、同じ機能を持つコードを簡単に再利用できます。

  3. 明確なインターフェース: メソッドを使用することで、構造体の操作方法が明確になります。

インターフェース

Go では、インターフェースを使用して、異なる型に共通のメソッドセットを定義できます。これにより、多態性を実現することができます。

package main

import "fmt"

type Shape interface {
    Area() int
}

type Rectangle struct {
    Width  int
    Height int
}

func (r Rectangle) Area() int {
    return r.Width * r.Height
}

type Circle struct {
    Radius int
}

func (c Circle) Area() int {
    return 3 * c.Radius * c.Radius
}

func printArea(s Shape) {
    fmt.Println("Area:", s.Area())
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    circle := Circle{Radius: 3}
    printArea(rect)
    printArea(circle)
}

このコードでは、Shape インターフェースを定義し、Rectangle と Circle の両方がこのインターフェースを実装しています。printArea 関数は Shape インターフェースを受け取り、任意の Shape の面積を計算します。

インターフェースのメリット

  1. 多態性の実現: インターフェースを使用することで、異なる型のオブジェクトを同じように扱うことができます。

  2. 柔軟な設計: インターフェースを使用することで、異なる実装を持つオブジェクトを簡単に交換できます。

  3. コードの拡張性: 新しい型を追加する際に、既存のコードを変更することなくインターフェースを実装することができます。

ジェネリクス

Go 1.18 以降、Go ではジェネリクスが導入され、型パラメータを使用して、より汎用的な関数やデータ構造を作成できるようになりました。

package main

import "fmt"

func Print[T any](s []T) {
    for _, v := range s {
        fmt.Println(v)
    }
}

func main() {
    ints := []int{1, 2, 3}
    strings := []string{"hello", "world"}

    Print(ints)
    Print(strings)
}

このコードでは、ジェネリック関数 Print を定義しています。Print 関数は任意の型 T を受け取ることができ、スライス内のすべての要素を出力します。

ジェネリクスのメリット

  1. コードの再利用性: ジェネリクスを使用することで、同じロジックを異なる型に対して再利用できます。

  2. 型安全性: ジェネリクスを使用することで、型安全なコードを記述することができ、ランタイムエラーを防ぐことができます。

  3. コードの簡潔性: ジェネリクスを使用することで、同じ処理を行うために複数の関数やデータ構造を定義する必要がなくなり、コードが簡潔になります。

ゴルーチンとチャネル

ゴルーチンの例

package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}

このコードでは、say 関数をゴルーチンとして実行しています。go キーワードを使うことで、並行して関数を実行できます。

チャネルの例

package main

import "fmt"

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    c <- sum
}

func main() {
    s := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c

    fmt.Println(x, y, x+y)
}

このコードでは、チャネルを使ってゴルーチン間でデータを送受信しています。c <- sum でチャネルにデータを送信し、<-c でチャネルからデータを受信します。

ゴルーチンとチャネルのメリット

  1. 並行処理の簡単な実装: ゴルーチンとチャネルを使うことで、複雑なスレッド管理を行わずに並行処理を実装できます。

  2. 効率的なリソース使用: ゴルーチンは非常に軽量であり、スレッドよりも少ないリソースで並行処理を実現できます。

  3. 安全なデータ共有: チャネルを使用することで、ゴルーチン間のデータ共有が安全に行えます。

デファード(defer)

defer ステートメントを使うと、関数の終了時に特定のコードを実行することができます。これはリソースのクリーンアップなどに便利です。

package main

import "fmt"

func main() {
    defer fmt.Println("world")

    fmt.Println("hello")
}

このコードでは、defer fmt.Println("world") により、main 関数が終了する直前に "world" が出力されます。

デファードのメリット

  1. リソース管理の簡素化: defer を使うことで、ファイルやネットワーク接続のクリーンアップコードを簡単に管理できます。

  2. コードの可読性向上: defer を使うことで、リソースのクリーンアップコードを関数の末尾に書く必要がなくなり、コードの可読性が向上します。

Discussion