2
1

Go・GORMチートシート

Last updated at Posted at 2024-09-09

本記事ではgo言語の超基礎を記載する.初めてgoを学ぶ人向けに最低限知ってほしいことのみまとめたので本記事を読み切ると基礎を学ぶ/復習することができます!おまけにgo向けのORMのgormチートシートもあります.

他のチートシート

git/ghコマンド

SQL

TypeScript

Docker コマンド

ステータスコード

プルリクエスト・マークダウン記法チートシート

ファイル操作コマンドチートシート

Vim

Goとは

Goは2007年にGoogleのRobert Griesemer, Rob Pike, Ken Thompsonによって設計され, 2009年に公開された比較的新しいプログラミング言語である. 大規模なソフトウェア開発における生産性と保守性の向上を目的として開発された.まずGoの特性について説明する.

gopherくん

goを学ぶにあたって最重要事項であるgopherくんを説明する.
image.png

gopherくんは, プログラミング言語Goのマスコットキャラクターである. このキャラクターは, 青色のゴファー(ホリネズミ)をモチーフにしており, シンプルでありながら愛らしいデザインが特徴的だ.

gopherくんのデザインは, 時を経て少しずつ進化してきた. 初期のバージョンはより複雑で詳細な描写だったが, 現在では単純化され, より抽象的な形態となっている. この変化は, Goの哲学である「シンプルさと効率性」を反映していると言える. gopherくんは, Goの公式ドキュメント, カンファレンス, ブログ記事, そしてさまざまなGoコミュニティのイベントで頻繁に使用されている.

また, gopherくんは単なるマスコットキャラクター以上の存在となっている. Goプログラマーたちの間では, 自分たちのプロジェクトやツールにgopherくんをアレンジしたバージョンを使用することが一種の伝統となっている. これにより, 多様なgopherくんのバリエーションが生まれ, Goコミュニティの創造性と結束を示す象徴ともなっている.

go文法の特徴

それではおまけ程度にgo文法の特徴を記載する.gopherくんでgoのほとんどを説明したつもりですが...

シンプルな文法と高速なコンパイル

Goは, C言語の系統を引き継ぎつつも, より簡潔で理解しやすい文法を採用している. これは, 大規模なプロジェクトにおいて, コードの可読性と保守性を高めるためである. また, 高速なコンパイルを実現するために, 循環的な依存関係を許可しないなど, 言語仕様レベルで工夫がなされている.

package main

import "fmt"

func main() {
    message := "Hello, Go!"
    fmt.Println(message)
}

この例からわかるように, 変数の型推論 (:= 演算子) や, セミコロンの省略など, 簡潔な記述が可能である.

静的型付けと型推論

Goは静的型付け言語であり, コンパイル時に型チェックが行われる. これにより, 実行時のエラーを減らし, プログラムの信頼性を高めている. 同時に, 型推論機能も備えており, 変数宣言時に型を明示的に指定する必要がない場合もある. これにより, 動的型付け言語の利点である簡潔な記述と, 静的型付け言語の安全性を両立させている.

package main

import "fmt"

func main() {
    var x int = 10        // 明示的な型指定
    y := 20               // 型推論 (int型と推論される)
    z := x + y            // 演算結果の型も推論される (int型)
    
    fmt.Printf("x: %T, y: %T, z: %T\n", x, y, z)
}

並行処理のサポート

Goは設計当初から並行処理を念頭に置いており, goroutineとchannelという概念を導入している. goroutineは軽量なスレッドのようなもので, 少ないリソースで多数の並行処理を実現できる. channelは goroutine 間でのデータの受け渡しを安全に行うための機構である.

package main

import (
    "fmt"
    "time"
)

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Printf("worker %d started job %d\n", id, j)
        time.Sleep(time.Second)
        fmt.Printf("worker %d finished job %d\n", id, j)
        results <- j * 2
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs)

    for a := 1; a <= 5; a++ {
        <-results
    }
}

この例では, 3つのworkerが並行して動作し, jobsチャネルからタスクを受け取り, 処理結果をresultsチャネルに送信している.

ガベージコレクション

Goは自動的なメモリ管理を行うガベージコレクション機能を備えている. これにより, 開発者はメモリ管理を意識する必要が少なくなり, メモリリークやダングリングポインタなどの問題を回避しやすくなっている. 同時に, Goのガベージコレクタは低レイテンシを実現するように設計されており, 実行時のパフォーマンスへの影響を最小限に抑えている.

インターフェースによる暗黙的な型の実装

Goのインターフェースは, 他の言語とは異なり, 明示的な宣言なしに実装できる. これにより, 柔軟なコード設計が可能になり, テストやモック作成も容易になる.

package main

import (
    "fmt"
    "math"
)

type Shape interface {
    Area() float64
}

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

type Rectangle struct {
    Width, Height float64
}

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

func printArea(s Shape) {
    fmt.Printf("面積: %0.2f\n", s.Area())
}

func main() {
    c := Circle{Radius: 5}
    r := Rectangle{Width: 3, Height: 4}

    printArea(c)
    printArea(r)
}

この例では, CircleとRectangleは明示的にShapeインターフェースを実装していないが, Area()メソッドを持っているため, Shapeとして扱うことができる.

エラー処理

Goは例外機構を持たず, 代わりに多値返却を利用したエラー処理を採用している. これにより, エラーの発生と処理を明示的に記述することが求められ, 堅牢なプログラムの作成につながる.

package main

import (
    "fmt"
    "errors"
)

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

func main() {
    result, err := divide(10, 0)
    if err != nil {
        fmt.Println("エラー:", err)
    } else {
        fmt.Println("結果:", result)
    }
}

クロスコンパイルのサポート

Goは設計当初からクロスコンパイルを考慮しており, 単一のソースコードから異なるOS, アーキテクチャ向けのバイナリを容易に生成できる. これにより, 開発環境と実行環境の差異を気にすることなく, 効率的な開発が可能になっている.

goの基本文法

ここからはgoの基本文法を紹介する

パッケージ

Goのプログラムはパッケージによって構成される.main パッケージは実行可能なプログラムの開始点となる.

// ここにパッケージを記載する.
import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("現在時刻:", time.Now())
}

このコードでは,fmt パッケージと time パッケージをインポートしている.main 関数内で,fmt.Println を使用して現在時刻を出力している.

Exported names

Goでは,大文字で始まる名前が外部からアクセス可能となる.

package main

import (
    "fmt"
    "math"
)

func main() {
    // このようにmath.PiのPiやSqrtがexported namesである.
    fmt.Println("円周率:", math.Pi)
    fmt.Println("2の平方根:", math.Sqrt(2))
}

このコードでは,math パッケージからエクスポートされた Pi と Sqrt を使用している.

変数

Goでは,var キーワードを使用して変数を宣言する.関数内では短縮形の := を使用できる.

package main

import "fmt"

var globalVar = "グローバル変数"

func main() {
    var localVar1 string = "ローカル変数1"
    localVar2 := "ローカル変数2"

    fmt.Println(globalVar)
    fmt.Println(localVar1)
    fmt.Println(localVar2)
}

この例では,異なる変数宣言の方法を示している.

基本型

Goには,bool,string,int,float64 などの基本型がある.

package main

import "fmt"

func main() {
    var b bool = true
    var s string = "Go言語"
    var i int = 42
    var f float64 = 3.14

    fmt.Printf("bool: %v\n", b)
    fmt.Printf("string: %v\n", s)
    fmt.Printf("int: %v\n", i)
    fmt.Printf("float64: %v\n", f)
}

このコードでは,異なる基本型の変数を宣言し,その値を出力している.

型変換

Goでは明示的な型変換が必要である.

1. 数値型間の変換

var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
    
fmt.Printf("int to float64: %v (%T)\n", f, f)
fmt.Printf("float64 to uint: %v (%T)\n", u, u)

2. 文字列と数値型の変換

// 文字列から数値型へ
s := "3.14"
if fValue, err := strconv.ParseFloat(s, 64); err == nil {
    fmt.Printf("string to float64: %v (%T)\n", fValue, fValue)
}

s = "42"
if iValue, err := strconv.Atoi(s); err == nil {
    fmt.Printf("string to int: %v (%T)\n", iValue, iValue)
}

// 数値型から文字列へ
i = 42
s = strconv.Itoa(i)
fmt.Printf("int to string: %v (%T)\n", s, s)

f = 3.14
s = fmt.Sprintf("%f", f)
fmt.Printf("float64 to string: %v (%T)\n", s, s)

3. バイトスライスと文字列の変換

bytes := []byte("Hello")
s = string(bytes)
fmt.Printf("[]byte to string: %v (%T)\n", s, s)

s = "World"
bytes = []byte(s)
fmt.Printf("string to []byte: %v (%T)\n", bytes, bytes)

4. runeと文字列の変換

r := 'A'
s = string(r)
fmt.Printf("rune to string: %v (%T)\n", s, s)

s = "B"
r = []rune(s)[0]
fmt.Printf("string to rune: %v (%T)\n", r, r)

5. インターフェースと具体型の変換

var i interface{} = "hello"
s, ok := i.(string)
fmt.Printf("interface to string: %v, %v (%T)\n", s, ok, s)

i = 42
iValue, ok := i.(int)
fmt.Printf("interface to int: %v, %v (%T)\n", iValue, ok, iValue)

6. boolと文字列の変換

b := true
s = strconv.FormatBool(b)
fmt.Printf("bool to string: %v (%T)\n", s, s)

s = "true"
if bValue, err := strconv.ParseBool(s); err == nil {
    fmt.Printf("string to bool: %v (%T)\n", bValue, bValue)

定数

Goでは const キーワードを使用して定数を宣言する.

package main

import "fmt"

// このように複数の定数をまとめて定義できる
const (
    Pi     = 3.14159
    MaxInt = 9223372036854775807
    Prefix = "GO_"
)

func main() {
    fmt.Println("円周率:", Pi)
    fmt.Println("int64の最大値:", MaxInt)
    fmt.Println("プレフィックス付きの文字列:", Prefix+"LANG")
}

ポインタ

ポインタとは
プログラミングにおいて変数のメモリアドレスを直接操作するための強力な機能

である.Goにおいて,ポインタは安全性と効率性を両立させる重要な要素となっている.

package main

import "fmt"

func main() {
    x := 10
    p := &x
    
    fmt.Printf("xの値: %d\n", x)
    fmt.Printf("xのアドレス: %p\n", p)
    fmt.Printf("pの指す値: %d\n", *p)
    
    *p = 20
    fmt.Printf("変更後のxの値: %d\n", x)
}

ポインタを理解することは,メモリ管理と効率的なプログラミングの基礎となる.変数のアドレスを取得し,そのアドレスを通じて値を操作することで,大きなデータ構造を効率的に扱うことができる.また,関数に値を渡す際にもポインタを使用することで,不必要なデータのコピーを避け,パフォーマンスを向上させることができる.

ポインタの使用には注意が必要だが,Goはポインタの安全性を高めるために,ポインタ演算を禁止するなどの制限を設けている.これにより,C言語などで頻繁に発生するポインタ関連のバグを大幅に減らすことができる.

構造体とインターフェース

1. 構造体の基本

構造体とは
異なる型のデータを一つの単位にまとめるための複合データ型

基本的な構造体の定義の仕方は以下の通りである.

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func main() {
    p := Person{Name: "Alice", Age: 30}
    fmt.Printf("%s is %d years old.\n", p.Name, p.Age)
}

この例では,NameとAgeフィールドを持つPerson構造体を定義している.構造体のフィールドは,ドット記法でアクセスできる.

2. 構造体のメソッド

構造体にメソッドを関連付けることで,データと操作を結びつけることができる.

type Rectangle struct {
    Width  float64
    Height float64
}

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

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    fmt.Printf("面積: %.2f\n", rect.Area())
}

この例では,Rectangle構造体にArea()メソッドを定義している.メソッドは,func (レシーバ) メソッド名(引数) 戻り値の形式で定義する.

3. 構造体の埋め込み

構造体を他の構造体に埋め込むことで,コードの再利用性を高めることができる.

type Animal struct {
    Name string
}

func (a Animal) Speak() string {
    return "Some sound"
}

type Dog struct {
    Animal
    Breed string
}

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

func main() {
    dog := Dog{Animal: Animal{Name: "Buddy"}, Breed: "Labrador"}
    fmt.Printf("%s is a %s and says %s\n", dog.Name, dog.Breed, dog.Speak())
}

この例では,Animal構造体をDog構造体に埋め込んでいる.Dogは,Animalのフィールドとメソッドを継承しつつ,独自のメソッドでオーバーライドすることができる.

4. 構造体タグ

構造体タグは,フィールドに対するメタデータを提供する.

type User struct {
    Name string `json:"name" validate:"required"`
    Age  int    `json:"age" validate:"gte=0,lte=130"`
}

構造体タグは,主にリフレクションと組み合わせて使用され,シリアライゼーションやバリデーションなどに活用される.

5. インターフェースの基本

インターフェースとは
メソッドのシグネチャの集まりを定義する.具体的な実装は提供せず,型が満たすべき振る舞いを規定する.

メソッドの定義の仕方は以下の通りである.

type Shape interface {
    Area() float64
}

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func PrintArea(s Shape) {
    fmt.Printf("面積: %.2f\n", s.Area())
}

func main() {
    circle := Circle{Radius: 5}
    PrintArea(circle)
}

この例では,Area()メソッドを持つShapeインターフェースを定義している.Circle構造体はArea()メソッドを実装しているため,暗黙的にShapeインターフェースを満たしている.

6. 空インターフェース

空インターフェース(interface{})は,任意の型の値を保持できる.

func PrintAny(v interface{}) {
    fmt.Printf("値: %v, 型: %T\n", v, v)
}

func main() {
    PrintAny(42)
    PrintAny("Hello")
    PrintAny(true)
}

空インターフェースは,型が事前にわからない値を扱う際に使用される.

7. 型アサーション

型アサーションは,インターフェース型の値が特定の具体型を保持しているかを確認する.

func process(i interface{}) {
    if v, ok := i.(string); ok {
        fmt.Printf("文字列: %s\n", v)
    } else if v, ok := i.(int); ok {
        fmt.Printf("整数: %d\n", v)
    } else {
        fmt.Printf("不明な型: %T\n", i)
    }
}

func main() {
    process("Hello")
    process(42)
    process(3.14)
}

型アサーションは,インターフェース値の具体的な型に基づいて処理を分岐させる際に使用される.

8. switchとインターフェースの組み合わせで型判定

インターフェース型の値の具体的な型を判別し, その型に応じて異なる処理を行う

func processValue(v interface{}) {
    switch val := v.(type) {
    case int:
        result := val * 2
        fmt.Printf("整数値 %d の2倍は %d である.\n", val, result)
    case string:
        length := len(val)
        fmt.Printf("文字列 '%s' の長さは %d である.\n", val, length)
    case []float64:
        sum := 0.0
        for _, num := range val {
            sum += num
        }
        average := sum / float64(len(val))
        fmt.Printf("浮動小数点数のスライスの平均値は %.2f である.\n", average)
    default:
        fmt.Println("未知の型である.")
    }
}

func main() {
    processValue(10)
    processValue("Hello, Go!")
    processValue([]float64{1.5, 2.7, 3.8, 4.2})
    processValue(true)
}

processValue 関数が interface{} 型の引数を受け取り, 型switchを使用して異なる型に対して異なる処理を行う. 整数の場合は2倍にし, 文字列の場合は長さを計算し, float64のスライスの場合は平均値を計算する. それ以外の型の場合はデフォルトメッセージを表示する.

main 関数では, 異なる型の値を processValue 関数に渡してその動作を確認している. これにより, 1つの関数で複数の型に対して適切な処理を行うことができ, 型に応じた柔軟な処理が可能になる.

9. インターフェースの埋め込み

インターフェースを他のインターフェースに埋め込むことで,より大きな抽象化を構築できる.

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type ReadWriter interface {
    Reader
    Writer
}

ReadWriterインターフェースは,ReaderとWriterの両方のメソッドを要求する.

10. ポリモーフィズムの実現

インターフェースを使用することで,異なる型に対して共通の操作を定義できる.

type Geometry interface {
    Area() float64
    Perimeter() float64
}

type Rectangle struct {
    Width, Height float64
}

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

func (r Rectangle) Perimeter() float64 {
    return 2*r.Width + 2*r.Height
}

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

func MeasureGeometry(g Geometry) {
    fmt.Printf("面積: %.2f, 周囲長: %.2f\n", g.Area(), g.Perimeter())
}

func main() {
    r := Rectangle{Width: 3, Height: 4}
    c := Circle{Radius: 5}

    MeasureGeometry(r)
    MeasureGeometry(c)
}

この例では,GeometryインターフェースをRectangleとCircleが実装している.MeasureGeometry関数は,このインターフェースを通じて異なる形状を統一的に扱うことができる.

構造体とインターフェース:高度な概念と応用

1. メモリレイアウトと最適化

Goの構造体は、メモリ上で連続して配置される。フィールドの順序を工夫することで、メモリ使用量を最適化できる。

type Inefficient struct {
    a bool    // 1バイト + 7バイトパディング
    b int64   // 8バイト
    c bool    // 1バイト + 7バイトパディング
}
// 合計: 24バイト

type Efficient struct {
    b int64   // 8バイト
    a bool    // 1バイト
    c bool    // 1バイト + 6バイトパディング
}
// 合計: 16バイト

2. unsafe.Offsetof

unsafe.Offsetof関数を使用して、構造体フィールドのメモリオフセットを取得できる。

import (
    "fmt"
    "unsafe"
)

func main() {
    var e Efficient
    fmt.Printf("b offset: %d\n", unsafe.Offsetof(e.b))
    fmt.Printf("a offset: %d\n", unsafe.Offsetof(e.a))
    fmt.Printf("c offset: %d\n", unsafe.Offsetof(e.c))
}

3. 構造体のゼロ値とポインタレシーバ

構造体のゼロ値は、各フィールドがそれぞれの型のゼロ値で初期化された状態。ポインタレシーバを使用することで、メソッド内で構造体を変更できる。

type Counter struct {
    count int
}

func (c *Counter) Increment() {
    c.count++
}

func (c Counter) Value() int {
    return c.count
}

func main() {
    var c Counter  // ゼロ値: {count: 0}
    c.Increment()
    fmt.Println(c.Value())  // 出力: 1
}

4. Option Pattern

構造体の初期化時にオプションを柔軟に設定するためのパターン。

type Server struct {
    host string
    port int
    timeout time.Duration
}

type ServerOption func(*Server)

func WithHost(host string) ServerOption {
    return func(s *Server) {
        s.host = host
    }
}

func WithPort(port int) ServerOption {
    return func(s *Server) {
        s.port = port
    }
}

func NewServer(options ...ServerOption) *Server {
    s := &Server{
        host: "localhost",
        port: 8080,
        timeout: 30 * time.Second,
    }
    for _, option := range options {
        option(s)
    }
    return s
}

func main() {
    server := NewServer(WithHost("example.com"), WithPort(9000))
}

5. Builder Pattern

複雑な構造体を段階的に構築するためのパターン。

type Person struct {
    name, address string
    age           int
}

type PersonBuilder struct {
    person *Person
}

func NewPersonBuilder() *PersonBuilder {
    return &PersonBuilder{person: &Person{}}
}

func (b *PersonBuilder) Name(name string) *PersonBuilder {
    b.person.name = name
    return b
}

func (b *PersonBuilder) Address(address string) *PersonBuilder {
    b.person.address = address
    return b
}

func (b *PersonBuilder) Age(age int) *PersonBuilder {
    b.person.age = age
    return b
}

func (b *PersonBuilder) Build() *Person {
    return b.person
}

func main() {
    person := NewPersonBuilder().
        Name("Alice").
        Address("123 Main St").
        Age(30).
        Build()
}

6. インターフェースの合成

複数のインターフェースを組み合わせて新しいインターフェースを作成する。

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

7. インターフェースの適応(Adapter Pattern)

既存の型を新しいインターフェースに適合させる。

type LegacyPrinter interface {
    Print(s string) string
}

type MyLegacyPrinter struct{}

func (lp *MyLegacyPrinter) Print(s string) string {
    return fmt.Sprintf("Legacy: %s", s)
}

type ModernPrinter interface {
    PrintModern() string
}

type LegacyPrinterAdapter struct {
    LegacyPrinter
}

func (a *LegacyPrinterAdapter) PrintModern() string {
    return a.Print("Adapted")
}

8. ジェネリクスとインターフェース(Go 1.18+)

ジェネリクスを使用して、型パラメータ付きのインターフェースを定義する。

type Comparable[T any] interface {
    Compare(T) int
}

type Integer int

func (i Integer) Compare(other Integer) int {
    if i < other {
        return -1
    } else if i > other {
        return 1
    }
    return 0
}

func Max[T Comparable[T]](a, b T) T {
    if a.Compare(b) >= 0 {
        return a
    }
    return b
}

func main() {
    fmt.Println(Max(Integer(5), Integer(3)))  // 出力: 5
}

9. 構造体のリフレクション

reflect パッケージを使用して、実行時に構造体の情報を検査する。

import "reflect"

func inspectStruct(s interface{}) {
    v := reflect.ValueOf(s)
    t := v.Type()

    for i := 0; i < v.NumField(); i++ {
        field := t.Field(i)
        value := v.Field(i)
        fmt.Printf("Field: %s, Type: %s, Value: %v\n", field.Name, field.Type, value.Interface())
    }
}

10. タグの解析

構造体タグを解析して、カスタムの振る舞いを実装する。

type User struct {
    Name string `json:"name" validate:"required"`
    Age  int    `json:"age" validate:"gte=0,lte=130"`
}

func parseTag(s interface{}, tagName string) {
    t := reflect.TypeOf(s)
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        tag := field.Tag.Get(tagName)
        fmt.Printf("Field: %s, %s tag: %s\n", field.Name, tagName, tag)
    }
}

11. ミューテックスを使用した構造体の同期

sync.Mutex を使用して、構造体のフィールドへの並行アクセスを同期する。

type SafeCounter struct {
    mu sync.Mutex
    count int
}

func (c *SafeCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

func (c *SafeCounter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

12. チャネルを使用した構造体の通信

構造体にチャネルを組み込み、ゴルーチン間で通信する.
チャネルとゴルーチンは詳しく後述

type Worker struct {
    tasks chan func()
    done  chan struct{}
}

func NewWorker() *Worker {
    w := &Worker{
        tasks: make(chan func()),
        done:  make(chan struct{}),
    }
    go w.run()
    return w
}

func (w *Worker) run() {
    for task := range w.tasks {
        task()
    }
    close(w.done)
}

func (w *Worker) Do(task func()) {
    w.tasks <- task
}

func (w *Worker) Stop() {
    close(w.tasks)
    <-w.done
}

これらの高度な概念と応用例は、Goにおける構造体とインターフェースの深い理解と効果的な使用方法を示しています。これらのテクニックを習得することで、より柔軟で強力、そして効率的なGoプログラムを書くことができます。

配列とスライス

ある型において複数の要素を持つ配列,スライスについて記載する.

1. 配列

配列は固定長であり, 一度宣言したサイズを変更することはできない. 以下に配列の宣言と使用例を示す.

package main

import "fmt"

func main() {
    // 配列の宣言と初期化
    var arr1 [3]int                     // ゼロ値で初期化される
    arr2 := [5]int{1, 2, 3, 4, 5}       // 初期値を指定
    arr3 := [...]string{"Go", "Java", "Python"} // サイズを自動で決定([...]の部分で、コンパイラに要素数を自動的に数えさせる)

    // 配列の要素にアクセス
    arr1[0] = 10
    fmt.Println(arr1[0]) // 10を出力

    // 配列の長さを取得
    fmt.Println(len(arr2)) // 5を出力

    // 配列の繰り返し処理
    for i, v := range arr3 {
        fmt.Printf("Index: %d, Value: %s\n", i, v)
    }
}

2. スライス

スライスは可変長であり, 動的に要素を追加したり削除したりできる. 以下にスライスの宣言と操作例を示す.

Pythonではリストと呼んでいるもの

package main

import "fmt"

func main() {
    // スライスの宣言と初期化
    slice1 := []int{1, 2, 3}
    slice2 := make([]int, 5, 10) // 長さ5, 容量10のスライス

    // スライスに要素を追加
    slice1 = append(slice1, 4, 5)
    fmt.Println(slice1) // [1 2 3 4 5]を出力

    // スライスの一部を取り出す
    subSlice := slice1[1:4]
    fmt.Println(subSlice) // [2 3 4]を出力

    // スライスの長さと容量を取得
    fmt.Printf("Length: %d, Capacity: %d\n", len(slice2), cap(slice2))

    // スライスのゼロ値
    var slice3 []int
    if slice3 == nil {
        fmt.Println("slice3 is nil")
    }

    // 2次元スライス
    matrix := [][]int{
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9},
    }
    fmt.Println(matrix[1][1]) // 5を出力

   // スライスのゼロ値はnilである. nilスライスは長さと容量が0で, 底層配列を持たない.

    var slice4 []int
    fmt.Println(slice, len(slice), cap(slice)) // [] 0 0を出力

    if slice == nil {
        fmt.Println("スライスはnilです")
    }

    // nilスライスと空スライスの違い
    emptySlice := []int{}
    fmt.Println(emptySlice == nil) // falseを出力
}

3. スライスと配列の違い

  1. サイズ: 配列は固定長, スライスは可変長である.
  2. 参照渡し: スライスは参照型, 配列は値型である.
  3. 柔軟性: スライスはより柔軟に操作できる.

スライスと配列の違いを示す例

package main

import "fmt"

func modifyArray(arr [3]int) {
    arr[0] = 100
}

func modifySlice(slice []int) {
    slice[0] = 100
}

func main() {
    // 配列
    arr := [3]int{1, 2, 3}
    modifyArray(arr)
    fmt.Println(arr) // [1 2 3]を出力 (変更されない)

    // スライス
    slice := []int{1, 2, 3}
    modifySlice(slice)
    fmt.Println(slice) // [100 2 3]を出力 (変更される)
}

この例から, 配列は値渡しであるため関数内での変更が元の配列に影響しないこと, スライスは参照渡しであるため関数内での変更が元のスライスに影響することがわかる.

4. make()関数によるスライスの作成

make()関数を使用して, 指定した長さと容量でスライスを作成できる.

package main

import "fmt"

func main() {
    slice := make([]int, 5, 10)
    fmt.Println(slice)         // [0 0 0 0 0]を出力
    fmt.Println(len(slice))    // 5を出力
    fmt.Println(cap(slice))    // 10を出力

    // 容量を指定しない場合, 長さと同じ値が使用される
    slice2 := make([]int, 3)
    fmt.Println(len(slice2))   // 3を出力
    fmt.Println(cap(slice2))   // 3を出力
}

スライスの長さと容量の関係

スライスは配列の一部を参照する柔軟なデータ構造ともいえる.スライスの動作を理解するには,長さ(length)と容量(capacity)の概念が重要.

1. 長さ(Length)

  • 定義: スライスに含まれる要素の数
  • 取得方法: len() 関数を使用
  • 特徴:
    • 0 から容量までの範囲内で変更可能
    • スライスの操作によって増減する

2. 容量(Capacity)

  • 定義: スライスの最初の要素から、元となる配列の最後の要素までの数
  • 取得方法: cap() 関数を使用
  • 特徴:
    • スライスが参照している底層配列の大きさを示す
    • スライスを拡張できる最大の長さを表す

底層配列(Underlying Array)とは

底層配列とは、スライスの基礎となる実際の配列のことを指す.スライスは本質的にこの底層配列への参照であると言える
つまり...

  • スライスは底層配列の一部または全部を参照する.
  • 複数のスライスが同じ底層配列を共有することができる.
  • 底層配列のサイズがスライスの容量(capacity)を決定する.
package main

import "fmt"

func main() {
   // 底層配列の作成
   underlyingArray := [5]int{10, 20, 30, 40, 50}

   // スライスの作成(底層配列の一部を参照)
   slice1 := underlyingArray[1:4]  // 20, 30, 40
   fmt.Println("slice1:", slice1)

   // 別のスライスを作成(同じ底層配列を参照)
   slice2 := underlyingArray[2:5]  // 30, 40, 50
   fmt.Println("slice2:", slice2)

   // slice1の要素を変更
   slice1[1] = 300

   // 底層配列と両方のスライスの内容を確認
   fmt.Println("底層配列:", underlyingArray)
   fmt.Println("slice1:", slice1)
   fmt.Println("slice2:", slice2)
}

// 出力:
// slice1: [20 30 40]
// slice2: [30 40 50]
// 底層配列: [10 20 300 40 50]
// slice1: [20 300 40]
// slice2: [300 40 50]
package main

import "fmt"

func main() {
    // 底層配列の定義
    arr := [...]string{"Golang", "Java", "Python", "Ruby", "JavaScript"}
    
    // スライスの作成
    slice := arr[1:4]
    
    fmt.Println("スライス:", slice)
    fmt.Println("長さ:", len(slice))
    fmt.Println("容量:", cap(slice))
    
    // スライスの拡張
    slice = slice[:4]
    fmt.Println("\n拡張後")
    fmt.Println("スライス:", slice)
    fmt.Println("長さ:", len(slice))
    fmt.Println("容量:", cap(slice))
    
    // 新しいスライスの作成
    newSlice := slice[1:3]
    fmt.Println("\n新しいスライス")
    fmt.Println("スライス:", newSlice)
    fmt.Println("長さ:", len(newSlice))
    fmt.Println("容量:", cap(newSlice))
}

このコードの出力は

スライス: [Java Python Ruby]
長さ: 3
容量: 4

拡張後
スライス: [Java Python Ruby JavaScript]
長さ: 4
容量: 4

新しいスライス
スライス: [Python Ruby]
長さ: 2
容量: 3
  1. 最初のスライス slice:

    • arr[1:4] で作成されたため、"Java"から始まる
    • 長さは3(Java, Python, Ruby)
    • 容量は4(Java, Python, Ruby, JavaScript)- 底層配列の残りの要素数
  2. スライスの拡張 slice[:4]:

    • 長さが4に増加(JavaScript が追加)
    • 容量は変わらず4(底層配列の上限に達している)
  3. 新しいスライス newSlice:

    • slice[1:3] で作成されたため、"Python"から始まる
    • 長さは2(Python, Ruby)
    • 容量は3(Python, Ruby, JavaScript)- この新しいスライスの開始位置から底層配列の終わりまでの要素数

重要なポイント

  1. スライスの容量は、そのスライスが参照している底層配列の残りの要素数によって決まる

  2. スライスを再スライスする際、新しいスライスの容量は元のスライスの残りの要素数になる

  3. スライスの長さは、実際に使用している要素数を示し、容量の範囲内で自由に変更できる

  4. append() 関数を使用してスライスに要素を追加する際、容量が足りない場合は新しい、より大きな底層配列が自動的に作成される

  5. 効率的なメモリ使用のためには、スライスの長さと容量を適切に管理することが重要

5. 連想配列(maps)

連想配列(maps)とは
キーと値のペアを格納するデータ構造
マップは非常に効率的なデータ構造で、キーを使って値を素早く検索、挿入、削除することができる

他の言語ではハッシュとかディクショナリとかいう

  • Maps(連想配列)の宣言と初期化
    make 関数を使用して bookInventory という名前の空のマップを作成している. このマップは文字列型のキーと整数型の値を持つ. 初期状態では要素が存在しないため, 空のマップが表示される.
package main

import (
    "fmt"
    "sort"
)

func main() {
    // Maps(連想配列)の宣言と初期化
    bookInventory := make(map[string]int)
    fmt.Println("空の本の在庫マップ:", bookInventory)
}
出力
空の本の在庫マップ: map[]
  • Maps(連想配列)への要素の挿入
    マップに新しい要素を追加している. キーに本のタイトル, 値に在庫数を設定している. この操作により, マップに3つの要素が追加される. 出力では, 追加された要素がキーと値のペアとして表示される.
// Maps(連想配列)への要素の挿入
bookInventory["The Go Programming Language"] = 10
bookInventory["Clean Code"] = 5
bookInventory["Design Patterns"] = 3
fmt.Println("初期の本の在庫:", bookInventory)
出力
初期の本の在庫: map[Clean Code:5 Design Patterns:3 The Go Programming Language:10]
  • Maps(連想配列)の要素の更新
    既存の要素の値を更新している. "The Go Programming Language" の在庫数を10から12に変更している. 更新後のマップを出力すると, 該当する要素の値が変更されていることが確認できる.
// Maps(連想配列)の要素の更新
bookInventory["The Go Programming Language"] = 12
fmt.Println("'The Go Programming Language'の在庫を更新:", bookInventory)
出力
'The Go Programming Language'の在庫を更新: map[Clean Code:5 Design Patterns:3 The Go Programming Language:12]
  • Maps(連想配列)からの要素の取得
    マップから特定のキーに対応する値を取得している. "The Go Programming Language" の在庫数を取得し, 変数 goBookCount に格納している. この値を出力すると, 更新後の在庫数12が表示される.
// Maps(連想配列)からの要素の取得
goBookCount := bookInventory["The Go Programming Language"]
fmt.Println("'The Go Programming Language'の在庫数:", goBookCount)
出力
'The Go Programming Language'の在庫数: 12
  • Maps(連想配列)の要素の存在確認
    マップ内に特定のキーが存在するかを確認している. "Clean Architecture" というキーの存在を確認し, 存在しない場合はその旨を出力している. この例では, キーが存在しないため, 「在庫リストにない」というメッセージが表示される.
    // Maps(連想配列)の要素の存在確認
    if count, exists := bookInventory["Clean Architecture"]; exists {
        fmt.Println("'Clean Architecture'の在庫数:", count)
    } else {
        fmt.Println("'Clean Architecture'は在庫リストにない")
    }
出力
'Clean Architecture'は在庫リストにない
  • Maps(連想配列)からの要素の削除
    delete 関数を使用してマップから要素を削除している. "Design Patterns" という要素を削除し, 削除後のマップを出力している. 出力結果から, 指定した要素が削除されていることが確認できる.
// Maps(連想配列)からの要素の削除
delete(bookInventory, "Design Patterns")
fmt.Println("'Design Patterns'を削除後の在庫:", bookInventory)
出力: 'Design Patterns'を削除後の在庫: map[Clean Code:5 The Go Programming Language:12]
  • Maps(連想配列)のサイズ(要素数)の取得
    len 関数を使用してマップの要素数を取得している. 削除操作の後, マップに残っている要素の数を出力している. この例では, 2つの要素が残っていることが確認できる.
// Maps(連想配列)のサイズ(要素数)の取得
fmt.Println("在庫リストの本の種類:", len(bookInventory))
出力
在庫リストの本の種類: 2

条件分岐文

1. If文

Goのif文は,条件式の前に初期化文を記述できる特徴がある.これにより,変数のスコープを限定し,コードの意図をより明確に表現できる.

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    rand.Seed(time.Now().UnixNano())

    if num := rand.Intn(10); num < 5 {
        fmt.Printf("%dは5未満です.\n", num)
    } else {
        fmt.Printf("%dは5以上です.\n", num)
    }
}

この例では,if文の条件式の前で乱数を生成し,その値に基づいて条件分岐を行っている.numという変数はif文のスコープ内でのみ有効であり,コードの他の部分に影響を与えない.

2. Switch文

Goのswitch文は,他の言語と比較して柔軟性が高い.特に,条件のないswitch文を使用することで,複雑な条件分岐を簡潔に表現できる.

package main

import (
    "fmt"
    "time"
)

func main() {
    t := time.Now()
    switch {
    case t.Hour() < 12:
        fmt.Println("おはようございます.")
    case t.Hour() < 17:
        fmt.Println("こんにちは.")
    default:
        fmt.Println("こんばんは.")
    }
}

この例では,現在の時刻に基づいて適切な挨拶を選択している.条件のないswitch文を使用することで,複数のif-else文を簡潔に表現している.

3. Defer文

defer文は,関数の終了時に特定の処理を実行することを保証する.これは,リソースの解放やクリーンアップ処理に特に有用である.

package main

import (
    "fmt"
    "os"
)

func main() {
    file, err := os.Create("test.txt")
    if err != nil {
        fmt.Println("ファイルの作成に失敗しました.")
        return
    }
    defer file.Close()

    _, err = file.WriteString("Hello, Go!")
    if err != nil {
        fmt.Println("ファイルの書き込みに失敗しました.")
        return
    }

    fmt.Println("ファイルへの書き込みが完了しました.")
}

この例では,defer文を使用してファイルのクローズ処理を保証している.これにより,関数が正常に終了した場合でも,エラーが発生した場合でも,確実にファイルがクローズされる.

繰り返し処理

Goでは、forキーワードを使用して様々な形式の繰り返し処理を実現できる。他の言語におけるwhile、do-while、for eachなどの概念も、Goのfor文を使って表現することができる。以下に、それぞれの概念に相当するGoの文法を示す。

1. 標準的なforループ

まず標準的なforループの書き方は以下の通りである.

package main

import "fmt"

func main() {
    for i := 0; i < 5; i++ {
        fmt.Printf("%d ", i)
    }
}

// 出力: 0 1 2 3 4

この形式は、多くの言語で見られる標準的なforループと同じである。

2. forを使ってwhile相当のループ

Goには専用のwhileキーワードは存在しないが、forを使って同等の処理を実現できる。

package main

import "fmt"

func main() {
    count := 0
    for count < 5 {
        fmt.Printf("%d ", count)
        count++
    }
}

// 出力: 0 1 2 3 4

この例では、条件式のみを持つforループを使用している。これは他の言語におけるwhileループと同等の動作をする。

3. 無限ループ

条件式を省略することで、無限ループを作成できる。これは、ループ内で明示的に終了条件をチェックする必要がある場合に使用する。

package main

import "fmt"

func main() {
    count := 0
    for {
        fmt.Printf("%d ", count)
        count++
        if count >= 5 {
            break
        }
    }
}

// 出力: 0 1 2 3 4

この形式は、他の言語におけるdo-whileループに近い動作を実現できる。ただし、Goには直接的なdo-whileループは存在しない。

4. rangeを使ってforループ(for eachの概念)

rangeについては次のセクションで詳しく説明します.

Goでは、rangeキーワードを使用することで、配列、スライス、マップ、文字列などの要素を順番に処理できる。これは他の言語におけるfor eachループに相当する。

package main

import "fmt"

func main() {
    fruits := []string{"りんご", "バナナ", "オレンジ"}
    for index, fruit := range fruits {
        fmt.Printf("%d: %s\n", index, fruit)
    }

    // インデックスが不要な場合は _ を使用する
    for _, fruit := range fruits {
        fmt.Println(fruit)
    }
}

// 出力:
// 0: りんご
// 1: バナナ
// 2: オレンジ
// りんご
// バナナ
// オレンジ

rangeを使用することで、コレクションの要素を簡単に反復処理できる。インデックスと値の両方を取得することも、値のみを取得することも可能である。

5. rangeのさまざまな使用例

rangeとは
主に反復処理を行うために使用される
主にスライス, 配列, マップ, チャネル, 文字列などのデータ構造を反復処理するために使用される. これにより, これらのデータ構造の要素に簡単にアクセスできる.

以下のrangeの説明ではここで定義したマップとスライスを用意する.

package main

import (
    "fmt"
    "sort"
)

func main() {
    // サンプルのマップとスライスを準備
    bookInventory := map[string]int{
        "The Go Programming Language": 12,
        "Clean Code": 5,
    }
    bookPrices := []float64{29.99, 39.99, 24.99, 49.99, 19.99}
}

```go
// Mapsに対するrangeの使用
fmt.Println("全ての本の在庫状況:")
for title, count := range bookInventory {
    fmt.Printf("%s: %d冊\n", title, count)
}
  • Mapsに対するrangeの使用
    range キーワードを使用してマップの全要素を反復処理している. キーと値のペアがそれぞれ titlecount 変数に格納され, 各本のタイトルと在庫数が出力される. この操作により, マップ内の全ての要素に簡単にアクセスできる.
出力
全ての本の在庫状況:
The Go Programming Language: 12冊
Clean Code: 5冊
}
  • キーでソートしてrangeで反復処理
    マップのキーをソートして表示するために, まずキーだけを取り出してスライスに格納している. 次に sort.Strings() 関数でこのスライスをソートし, ソートされたキーの順番でマップの要素にアクセスしている. これにより, アルファベット順で本の在庫状況を表示できる.
// キーでソートしてrangeで反復処理
var sortedTitles []string
for title := range bookInventory {
    sortedTitles = append(sortedTitles, title)
}
sort.Strings(sortedTitles)

fmt.Println("\nアルファベット順の本の在庫状況:")
for _, title := range sortedTitles {
    fmt.Printf("%s: %d冊\n", title, bookInventory[title])
}
出力
アルファベット順の本の在庫状況:
Clean Code: 5冊
The Go Programming Language: 12冊
  • スライスに対するrangeの使用
    range キーワードをスライスに対して使用している. インデックスと値のペアが indexprice 変数に格納され, 各本の番号と価格が出力される. この方法により, スライスの全要素に簡単にアクセスできる.
// スライスに対するrangeの使用
fmt.Println("\n本の価格リスト:")
for index, price := range bookPrices {
    fmt.Printf("本%d: %.2f円\n", index+1, price)
}
出力
本の価格リスト:
本1: 29.99円
本2: 39.99円
本3: 24.99円
本4: 49.99円
本5: 19.99円
  • rangeで合計値を計算
    range を使用してスライスの全要素を反復処理し, 合計値を計算している. インデックスは不要なので _ で無視し, 価格のみを使用している. この方法により, スライス内の全ての値を効率的に合計できる.
// rangeで合計値を計算
totalPrice := 0.0
for _, price := range bookPrices {
    totalPrice += price
}
fmt.Printf("本の合計価格: %.2f円\n", totalPrice)
出力
本の合計価格: 164.95円

7. breakとcontinueの使用

breakとcontinueについて
breakとcontinueは, Goを含む多くのプログラミング言語で使用される制御フロー文である. これらは主にループ内で使用され, ループの実行を制御するために用いられる

break
break はループの実行を即座に終了させ, ループの次の文から処理を継続する.

  • 使用場面

    • 特定の条件が満たされた時にループを早期に終了させたい場合.
    • 無限ループから抜け出す必要がある場合.
  • 影響範囲
    break は, それが含まれる最も内側のループのみを終了させる. ネストされたループの場合, 外側のループは影響を受けない

for i := 0; i < 10; i++ {
   if i == 5 {
       break
   }
   fmt.Println(i)
}
// 0から4まで出力され, 5で終了する

continue
continue は現在の反復をスキップし, ループの次の反復に直接移動する.

  • 使用場面
    • 特定の条件下でループの残りの部分をスキップしたい場合.
    • 特定の要素を処理から除外したい場合.
  • 影響範囲
    continue は現在の反復のみに影響し, ループ全体は継続して実行される.
for i := 0; i < 5; i++ {
    if i == 2 {
        continue
    }
    fmt.Println(i)
}
// 0, 1, 3, 4が出力され, 2はスキップされる

continue,breakを併用する例
range と条件文を組み合わせて, 特定の条件を満たす要素のみを処理している. continue を使用して30円未満の価格をスキップし, break を使用して45円以上の価格で処理を停止している. これにより, 特定の条件下での反復処理の制御が可能になる.

// breakとcontinueの使用
fmt.Println("\n30円以上の本の価格を表示(45円以上で停止):")
for _, price := range bookPrices {
    if price < 30.0 {
        continue
    }
    if price >= 45.0 {
        fmt.Printf("%.2f円 - 45円以上なので停止\n", price)
        break
    }
    fmt.Printf("%.2f円\n", price)
}
出力
30円以上の本の価格を表示(45円以上で停止):
39.99円
49.99円 - 45円以上なので停止
}

関数

Goの関数は,プログラムの基本的な構成要素であり,コードの再利用性と可読性を高める重要な役割を果たす.以下,Goの関数の特徴と使用方法について,より具体的な例を交えて詳しく説明する.

1. 基本的な関数の定義と使用

関数は以下の形式で定義される.

func 関数名(引数リスト) 戻り値の型 {
    // 関数の本体
}

例えば,文字列を受け取り,その長さを返す関数は次のように定義できる.

package main

import (
    "fmt"
    "strings"
)

func wordCount(s string) int {
    // Fieldsは"strings" パッケージの関数で,文字列を空白文字(スペース,タブ,改行など)で分割し,結果を文字列のスライスで返す
    words := strings.Fields(s)
    // len関数はスライスの要素数を出力する
    return len(words)
}

func main() {
    sentence := "Go言語は簡潔で効率的なプログラミング言語です"
    count := wordCount(sentence)
    fmt.Printf("文章「%s」の単語数: %d\n", sentence, count)
    // 出力: 文章「Go言語は簡潔で効率的なプログラミング言語です」の単語数: 7
}

この例では,wordCount関数が文字列を受け取り,それに含まれる単語の数を返している.

2. 複数の戻り値とエラーハンドリング

Goの関数は複数の値を返すことができ,これはエラー処理に特に有用である.慣例として,エラーは最後の戻り値として返される.goでは返り値にerrorを含ませることが慣習である.これはgoが例外処理を持たないからである.

package main

import (
    "fmt"
    "strconv"
)

func parseAndMultiply(s1, s2 string) (int, error) {
    // "Atoi"は"strconv" パッケージの関数で, "ASCII to Integer" の略.文字列を整数に変換する関数
    n1, err := strconv.Atoi(s1)
    if err != nil {
        return 0, fmt.Errorf("最初の引数のパースに失敗: %v", err)
    }
    
    n2, err := strconv.Atoi(s2)
    if err != nil {
        return 0, fmt.Errorf("2番目の引数のパースに失敗: %v", err)
    }
    
    return n1 * n2, nil
}

func main() {
    result, err := parseAndMultiply("10", "5")
    // このようにエラーハンドリングをする
    if err != nil {
        fmt.Println("エラー:", err)
    } else {
        fmt.Printf("10 * 5 = %d\n", result)
    }
    
    result, err = parseAndMultiply("abc", "5")
    if err != nil {
        fmt.Println("エラー:", err)
    } else {
        fmt.Printf("結果: %d\n", result)
    }
}

// 出力:
// 10 * 5 = 50
// エラー: 最初の引数のパースに失敗: strconv.Atoi: parsing "abc": invalid syntax

この例では,parseAndMultiply関数が2つの文字列を受け取り,それらを整数に変換して掛け算を行う.エラーが発生した場合は,適切なエラーメッセージと共にエラーを返す.

3. 名前付き戻り値と裸のreturn

名前付き戻り値を使用すると,関数の目的がより明確になり,短い関数では裸のreturn文を使用できる.

package main

import (
    "fmt"
    "strings"
)

func processName(fullName string) (firstName, lastName string, err error) {
    // Splitは"strings" パッケージの関数で,第一引数の文字列を、第二引数の区切り文字で分割し、結果を文字列のスライスとして返す.
    names := strings.Split(fullName, " ")
    if len(names) != 2 {
        err = fmt.Errorf("名前のフォーマットが不正です: %s", fullName)
        return
    }
    firstName = names[0]
    lastName = names[1]
    return // 裸のreturn
}

func main() {
    first, last, err := processName("John Doe")
    if err != nil {
        fmt.Println("エラー:", err)
    } else {
        fmt.Printf("姓: %s, 名: %s\n", last, first)
    }
    
    first, last, err = processName("Jane Smith Johnson")
    if err != nil {
        fmt.Println("エラー:", err)
    } else {
        fmt.Printf("姓: %s, 名: %s\n", last, first)
    }
}

// 出力:
// 姓: Doe, 名: John
// エラー: 名前のフォーマットが不正です: Jane Smith Johnson

この例では,processName関数が名前付き戻り値を使用している.エラーが発生した場合や正常に処理が完了した場合に,裸のreturn文で値を返している.

4. 可変長引数と関数の値渡し

Goの関数は可変長の引数を受け取ることができ,また関数自体を引数として渡すこともできる.

package main

import (
    "fmt"
    "strings"
)

func applyToStrings(f func(string) string, strings ...string) []string {
    // スライスを作成するための組み込み関数.型、長さ、そして任意でキャパシティを指定できる.
    result := make([]string, len(strings))
    for i, s := range strings {
        result[i] = f(s)
    }
    return result
}

func main() {
    upperCase := func(s string) string { return strings.ToUpper(s) }
    lowerCase := func(s string) string { return strings.ToLower(s) }
    
    fruits := []string{"Apple", "Banana", "Cherry"}
    
    upperFruits := applyToStrings(upperCase, fruits...)
    fmt.Println("大文字化:", upperFruits)
    
    lowerFruits := applyToStrings(lowerCase, fruits...)
    fmt.Println("小文字化:", lowerFruits)
}

// 出力:
// 大文字化: [APPLE BANANA CHERRY]
// 小文字化: [apple banana cherry]

この例では,applyToStrings関数が文字列を処理する関数と可変長の文字列引数を受け取り,各文字列に対してその関数を適用している.main関数では,無名関数を定義し,それらを引数として渡している.

5. クロージャと状態の保持

クロージャは,関数が定義された環境の変数を捕捉し,状態を保持することができる強力な機能である.以下の例では,カウンターを生成する関数を示す.

package main

import "fmt"

func createCounter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

func main() {
    counter1 := createCounter()
    counter2 := createCounter()

    fmt.Println(counter1()) // 1
    fmt.Println(counter1()) // 2
    fmt.Println(counter2()) // 1
    fmt.Println(counter1()) // 3
}

この例では,createCounter関数がクロージャを返している.各クロージャは独自のcount変数を保持し,呼び出されるたびにその値を増加させる.

6. 関数型と高階関数

Goでは関数を型として扱うことができ,これにより高階関数(関数を引数として受け取るか,関数を返す関数)を実装できる.

package main

import (
    "fmt"
    "strings"
)

type StringProcessor func(string) string

func pipeline(processors ...StringProcessor) StringProcessor {
    return func(s string) string {
        result := s
        for _, proc := range processors {
            result = proc(result)
        }
        return result
    }
}

func main() {
    removeSpaces := func(s string) string {
        return strings.ReplaceAll(s, " ", "")
    }
    toLowerCase := strings.ToLower
    reverseString := func(s string) string {
        runes := []rune(s)
        for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
            runes[i], runes[j] = runes[j], runes[i]
        }
        return string(runes)
    }

    process := pipeline(removeSpaces, toLowerCase, reverseString)
    result := process("Hello, World!")
    fmt.Println(result) // !dlrowolleh
}

この例では,複数の文字列処理関数を組み合わせてパイプラインを作成している.pipelineは可変長の関数を受け取り,それらを順番に適用する新しい関数を返す.

7. ジェネリクス関数

Go 1.18以降では,ジェネリクスがサポートされており,型パラメータを使用して汎用的な関数を書くことができる.

package main

import (
    "fmt"
    "golang.org/x/exp/constraints"
)

func Map[T, U any](slice []T, f func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = f(v)
    }
    return result
}

func Sum[T constraints.Ordered](slice []T) T {
    var sum T
    for _, v := range slice {
        sum += v
    }
    return sum
}

func main() {
    numbers := []int{1, 2, 3, 4, 5}
    squares := Map(numbers, func(x int) int { return x * x })
    fmt.Println(squares) // [1 4 9 16 25]

    sum := Sum(numbers)
    fmt.Println(sum) // 15

    words := []string{"hello", "world"}
    lengths := Map(words, func(s string) int { return len(s) })
    fmt.Println(lengths) // [5 5]
}

この例では,MapとSumという2つのジェネリクス関数を定義している.Mapは任意の型のスライスと変換関数を受け取り,変換後のスライスを返す.Sumは数値型のスライスの合計を計算する.constraints.Orderedは,golang.org/x/exp/constraintsパッケージで定義されている制約で,順序付け可能な型(整数,浮動小数点数,文字列など)を表す.

goの並行処理について(goroutine)

goroutine(ゴルーチン)は
Go言語における並行処理の基本単位である.軽量なスレッドとして実装され,効率的な並行処理を可能にする.

goroutineは,Go言語のランタイムによって管理される軽量なスレッドである.OSのスレッドよりも少ないリソースで動作し,数千から数百万のgoroutineを同時に実行できる.

1. goroutineの定義

goroutineは,関数またはメソッドの呼び出しの前にgoキーワードを付けることで起動できる.

構文:

go 関数名(引数, ...)

例:

go myFunction(arg1, arg2)

goキーワードは新しいgoroutineを生成し,指定された関数をその中で実行する.goroutineは並行に動作するため,関数の実行終了を待つことなく,次の処理に進む.

2. goroutineの終了条件

goroutineは以下の条件で終了する:

1. 関数の処理が完了する
2. return文で関数から抜ける
3. runtime.Goexit()を実行する

3. goroutineの数の取得

現在実行中のgoroutineの数は,runtime.NumGoroutine()関数を使用して取得できる.

package main

import (
    "fmt"
    "runtime"
)

func main() {
    fmt.Println("現在のgoroutine数:", runtime.NumGoroutine())
    go func() {
        // 新しいgoroutine
    }()
    fmt.Println("goroutine追加後:", runtime.NumGoroutine())
}
出力
現在のgoroutine数: 1
goroutine追加後: 2

4. goroutineの実践的な例

printNumbers関数を2つのgoroutineで並行に実行している.メイン関数は1秒待機して,goroutineの実行を許可している.

package main

import (
    "fmt"
    "time"
)

func printNumbers(prefix string) {
    for i := 1; i <= 5; i++ {
        fmt.Printf("%s: %d\n", prefix, i)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    fmt.Println("開始")
    go printNumbers("A")
    go printNumbers("B")
    time.Sleep(1 * time.Second)
    fmt.Println("終了")
}
出力
開始
A: 1
B: 1
A: 2
B: 2
A: 3
B: 3
A: 4
B: 4
A: 5
B: 5
終了

5. channel(チャネル)による同期

goroutine間の通信や同期には,channel(チャネル)を使用する.channelは,goroutine間でデータを安全に送受信するための機構である.

channelの作成

channelはmake関数を使用して作成する.

構文:

ch := make(chan )
ch := make(chan , バッファサイズ)

例:

messages := make(chan string)
buffer := make(chan int, 10)

channelでの送受信

channelを使用して値を送受信するには,チャネルオペレータ<-を使用する.

送信:

channel <- 

受信:

変数 := <-channel

例:

package main

import "fmt"

func main() {
    ch := make(chan string)
    go func() {
        ch <- "Hello, Channel!"
    }()
    message := <-ch
    fmt.Println(message)
}

出力
Hello, Channel!

はじめに,文字列型のチャネル 'ch' が作成され,新しいゴルーチンが起動され,そのゴルーチン内でチャネルにメッセージが送信される.メインゴルーチンはチャネルからメッセージを受信するまでブロックされる.メッセージが受信されると,それが 'message' 変数に格納される.最後に,受信したメッセージが出力される.

goroutineの同期

channelを使用して,goroutineの実行完了を待つことができる.

例:

package main

import (
    "fmt"
    "time"
)

func worker(id int, done chan bool) {
    fmt.Printf("Worker %d 開始\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d 終了\n", id)
    done <- true
}

func main() {
    done := make(chan bool, 2)
    go worker(1, done)
    go worker(2, done)

    <-done
    <-done
    fmt.Println("全てのworkerが終了")
}
出力
Worker 1 開始
Worker 2 開始
Worker 2 終了
Worker 1 終了
全てのworkerが終了

この例では,2つのworkerをgoroutineで実行し,それぞれの完了をdonechannelで待っている.

6. select文による複数channelの待機

select文を使用すると,複数のchannelの操作を同時に待つことができる.
以下のコードはselectステートメントを使用して複数のチャネルを同時に監視する方法.selectは,準備ができた(データを受信できる)チャネルからデータを受信する.これにより,複数の非同期操作を効率的に処理することができる.

例:

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(2 * time.Second)
        ch1 <- "channel 1"
    }()

    go func() {
        time.Sleep(1 * time.Second)
        ch2 <- "channel 2"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println("受信1:", msg1)
        case msg2 := <-ch2:
            fmt.Println("受信2:", msg2)
        }
    }
}
出力
受信2: channel 2
受信1: channel 1

処理の流れ

  1. 2つの無バッファチャネル ch1 と ch2 を作成
  2. 2つのゴルーチンを起動し,それぞれ異なる時間スリープした後にチャネルにメッセージを送信
    • 1つ目のゴルーチンは2秒後に ch1 にメッセージを送信
    • 2つ目のゴルーチンは1秒後に ch2 にメッセージを送信
  3. メインゴルーチンでは、selectステートメントを使用して両方のチャネルからの受信を待つ
  4. ch2 からの受信が先に行われるため、"受信2: channel 2" が最初に出力される
  5. その後、ch1 からの受信が行われ、"受信1: channel 1" が出力される

gormチートシート

ここからはGo言語のためのオブジェクトリレーショナルマッピング(ORM)ライブラリであるgormについて記載する.
gormはフルフィーチャーのORMライブラリであり,データベース操作を簡素化し,Goの構造体とデータベースのテーブルをマッピングする機能がある.

主な機能

  • 複数のデータベース対応(MySQL, PostgreSQL, SQLite, SQL Server など)
  • 自動マイグレーション
  • CRUD操作の簡素化
  • 関連付け(一対一、一対多、多対多)
  • トランザクションサポート
  • 複合主キー
  • SQL Builder
  • 自動タイムスタンプ(CreatedAt, UpdatedAt)
  • ソフトデリート
  • プリローディングとレイジーローディング

データベース接続

SQL:

なし(ドライバー固有の接続方法)

gorm:

db, err := gorm.Open(mysql.Open("user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"), &gorm.Config{})

データベースへの接続を確立する.gormでは,使用するデータベースに応じたドライバーを指定し,接続情報を与える.

テーブル作成

SQL

CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), email VARCHAR(255))

gorm:

db.AutoMigrate(&User{})

モデル(構造体)

type User struct {
    ID    uint   `gorm:"primaryKey;autoIncrement"`
    Name  string `gorm:"size:255"`
    Email string `gorm:"size:255"`
}

データベースにテーブルを作成する.gormでは,構造体を定義し,AutoMigrateを使用することで,その構造体に基づいてテーブルが自動的に作成または更新される.

レコード作成

SQL

INSERT INTO users (name, email) VALUES ('John Doe', 'john@example.com')

gorm

db.Create(&User{Name: "John Doe", Email: "john@example.com"})

テーブルに新しいレコードを挿入する.gormでは,構造体のインスタンスを作成し,Createメソッドを使用する.

単一レコード取得:

SQL

SELECT * FROM users WHERE id = 1 LIMIT 1
db.First(&user, 1)

特定のIDを持つ単一のレコードを取得する.gormのFirstメソッドは,自動的にプライマリキーを使用して検索を行う.

条件付きレコード取得

SQL

SELECT * FROM users WHERE name = 'John Doe'

gorm

db.Where("name = ?", "John Doe").Find(&users)

特定の条件に合致するレコードを取得する.gormでは,Whereメソッドを使用して条件を指定し,Findメソッドで結果を取得する.

特定カラムの選択

SQL:

SELECT name, email FROM users

gorm

db.Select("name", "email").Find(&users)

特定のカラムのみを選択してレコードを取得する.gormでは,Selectメソッドを使用して取得するカラムを指定できる.

レコード更新

SQL

UPDATE users SET email = 'newemail@example.com' WHERE id = 1

gorm

db.Model(&user).Where("id = ?", 1).Update("email", "newemail@example.com")

既存のレコードを更新する.gormでは,Modelメソッドで対象を指定し,Updateメソッドで更新を行う.

レコード削除

SQL

DELETE FROM users WHERE id = 1

gorm

db.Delete(&user, 1)

レコードを削除する.gormのDeleteメソッドは,ソフトデリート(論理削除)がモデルで定義されている場合はソフトデリートを行い,そうでない場合は物理削除を行う.

レコード数取得

SQL

SELECT COUNT(*) FROM users

gorm

db.Model(&User{}).Count(&count)

テーブル内のレコード数を取得する.gormでは,Countメソッドを使用する.

順序付け

SQL

SELECT * FROM users ORDER BY name DESC

gorm

db.Order("name DESC").Find(&users)

結果を特定の順序で取得する.gormのOrderメソッドを使用して,ソート順を指定できる.

制限とオフセット

SQL

SELECT * FROM users LIMIT 10 OFFSET 5

gorm

db.Limit(10).Offset(5).Find(&users)

結果の数を制限し,特定の位置から取得を開始する.gormでは,LimitとOffsetメソッドを使用する.

グループ化

SQL

SELECT country, COUNT(*) FROM users GROUP BY country

gorm

db.Model(&User{}).Select("country, count(*) as user_count").Group("country").Find(&result)

結果をグループ化し,集計を行う.gormでは,GroupとSelectメソッドを組み合わせて使用する.

結合

SQL:

SELECT users.name, orders.amount FROM users JOIN orders ON users.id = orders.user_id

gorm:

db.Table("users").Select("users.name, orders.amount").Joins("JOIN orders ON users.id = orders.user_id").Scan(&result)

複数のテーブルを結合してデータを取得する.gormでは,Joinsメソッドを使用してテーブルを結合できる.

サブクエリ

SQL:

SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount > 1000)

gorm

db.Where("id IN (?)", db.Table("orders").Select("user_id").Where("amount > ?", 1000)).Find(&users)

サブクエリを使用して複雑な条件を指定する.gormでは,別のクエリを条件として渡すことでサブクエリを実現できる.

トランザクション:

SQL:

BEGIN; [一連の操作]; COMMIT;

gorm:

db.Transaction(func(tx *gorm.DB) error {
    // トランザクション内の操作
    if err := tx.Create(&user).Error; err != nil {
        return err
    }
    return nil
})

複数の操作をアトミックに実行する.gormのTransactionメソッドを使用することで,エラーが発生した場合に自動的にロールバックされる.

集計関数

SQL:

SELECT AVG(age) FROM users

gorm

db.Model(&User{}).Select("AVG(age)").Row().Scan(&result)

平均値などの集計値を計算する.gormでは,Selectメソッドで集計関数を指定し,Row()とScan()メソッドを使用して結果を取得する.

重複排除

SQL:

SELECT DISTINCT name FROM users

gorm:

db.Distinct("name").Find(&names)

重複を除いた結果を取得する.gormのDistinctメソッドを使用する.

HAVING句:

SQL:

SELECT country, COUNT(*) FROM users GROUP BY country HAVING COUNT(*) > 5

gorm

db.Model(&User{}).Select("country, COUNT(*) as user_count").Group("country").Having("COUNT(*) > ?", 5).Find(&result)

グループ化された結果に対して条件を適用する.gormでは,Havingメソッドを使用する.

インデックス作成

SQL:

CREATE INDEX idx_name ON users(name)

gorm

db.Migrator().CreateIndex(&User{}, "idx_name")

テーブルにインデックスを作成する.gormでは,Migratorインターフェースを通じてインデックスを作成できる.

カラム追加

SQL:

ALTER TABLE users ADD COLUMN age INT

gorm

db.Migrator().AddColumn(&User{}, "Age")

既存のテーブルに新しいカラムを追加する.gormでは,Migratorインターフェースを使用してスキーマの変更を行える.

関連付け(一対多)

SQL

SELECT * FROM users JOIN orders ON users.id = orders.user_id WHERE users.id = 1

gorm

db.Preload("Orders").First(&user, 1)

関連するテーブルのデータを一緒に取得する.gormのPreloadメソッドを使用することで,関連データを効率的に取得できる.

関連付け(多対多)

SQL

SELECT * FROM users JOIN user_languages ON users.id = user_languages.user_id JOIN languages ON user_languages.language_id = languages.id WHERE users.id = 1

gorm

db.Preload("Languages").First(&user, 1)

多対多の関係にあるデータを取得する.gormでは,一対多の場合と同様にPreloadメソッドを使用できる.

ソフトデリート

SQL

UPDATE users SET deleted_at = CURRENT_TIMESTAMP WHERE id = 1

gorm

db.Delete(&user)

レコードを論理的に削除する.gormでは,モデルにgorm.DeletedAtフィールドを追加することで自動的にソフトデリートが有効になる.

ハードデリート

SQL

DELETE FROM users WHERE id = 1

gorm

db.Unscoped().Delete(&user)

レコードを物理的に削除する.gormでは,Unscopedメソッドを使用することでソフトデリートを無視して完全に削除できる.

gormの活用例

package main

import (
    "fmt"
    "log"

    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

// モデル定義
type User struct {
    gorm.Model
    Name   string
    Email  string
    Orders []Order
}

type Order struct {
    gorm.Model
    UserID uint
    Amount float64
}

func main() {
    // データベース接続
    dsn := "user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatal("データベース接続に失敗しました:", err)
    }

    // マイグレーション
    db.AutoMigrate(&User{}, &Order{})

    // ユーザー作成
    user := User{Name: "John Doe", Email: "john@example.com"}
    result := db.Create(&user)
    if result.Error != nil {
        log.Fatal("ユーザー作成に失敗しました:", result.Error)
    }
    fmt.Printf("ユーザーが作成されました: ID = %d\n", user.ID)

    // 注文作成
    order := Order{UserID: user.ID, Amount: 100.50}
    result = db.Create(&order)
    if result.Error != nil {
        log.Fatal("注文作成に失敗しました:", result.Error)
    }
    fmt.Printf("注文が作成されました: ID = %d\n", order.ID)

    // ユーザーと注文を取得
    var retrievedUser User
    result = db.Preload("Orders").First(&retrievedUser, user.ID)
    if result.Error != nil {
        log.Fatal("ユーザー取得に失敗しました:", result.Error)
    }

    fmt.Printf("取得したユーザー: %s, Email: %s\n", retrievedUser.Name, retrievedUser.Email)
    fmt.Printf("注文数: %d\n", len(retrievedUser.Orders))
    for _, o := range retrievedUser.Orders {
        fmt.Printf("  注文ID: %d, 金額: %.2f\n", o.ID, o.Amount)
    }

    // ユーザー更新
    result = db.Model(&user).Update("Email", "john.doe@example.com")
    if result.Error != nil {
        log.Fatal("ユーザー更新に失敗しました:", result.Error)
    }
    fmt.Println("ユーザーのメールアドレスが更新されました")

    // 全ユーザー取得
    var users []User
    result = db.Find(&users)
    if result.Error != nil {
        log.Fatal("ユーザー一覧取得に失敗しました:", result.Error)
    }
    fmt.Printf("ユーザー総数: %d\n", len(users))

    // ユーザー削除
    result = db.Delete(&user)
    if result.Error != nil {
        log.Fatal("ユーザー削除に失敗しました:", result.Error)
    }
    fmt.Println("ユーザーが削除されました")
}

このようにgormは,Go言語でのデータベース操作を簡素化し,開発者が効率的にデータベース操作を行えるようにする強力なツールである.ただし,非常に複雑なクエリや特定のデータベース固有の機能を使用する場合は,Raw SQLを使用する必要がある場合もあることに注意が必要である.

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1