Goのポインタについて

ポインタはGoの基本的な概念で、プログラム内で値やレコードへの参照を渡すことができるようになります。 Goの重要な機能となります。

Goでは、ポインタは他の変数のメモリアドレスを格納する変数です。ポインタを作るには、&演算子を使って変数のアドレスを取得します。

package main

import "fmt"

func main() {
    x := 10
    var p *int = &x

    fmt.Println(p)  // x のメモリアドレスを出力します
    fmt.Println(*p) // x の値を出力します
}

上記のコードでは、値10を持つ変数xとxへのポインタpを作成し、&演算子でpに格納されているxのメモリアドレスを返しています。 演算子を使ってポインタの参照を解除し、そのポインタが指す値にアクセスすることができます。上記のコードでは、*pはxの値、つまり10を返します。

ポインタは、関数同士で値への参照を渡すために使用することができます。

package main

import "fmt"

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

func main() {
    x := 10
    addOne(&x)
    fmt.Println(x) 
}

上記のコードでは、addOne関数にxのメモリアドレスをポインタとして渡しています。関数内部では、*演算子を使ってポインタをデリファレンスし、xの値を変更しています。

メソッドにおけるポインタのレシーバー。Goでは、ポインタを含む任意の型に対してメソッドを定義することができます。ポインタのレシーバを持つメソッドを定義した場合、そのメソッドはレシーバの値を変更できることを意味します。

package main

import "fmt"

type MyStruct struct {
    x int
}

func (s *MyStruct) increment() {
    s.x++
}

func main() {
    s := MyStruct{x: 10}
    s.increment()
    fmt.Println(s.x) 
}

上記のコードでは、ポインタ・レシーバを持つMyStructのメソッドincrementを定義しています。もし、このメソッドを値のレシーバで定義していたら(例:func (s MyStruct) increment())、sの値を変更することはできません。

ポインタ型

Goでは、他の型へのポインタである新しい型を定義することができます。

package main

import "fmt"

type MyInt int
type MyIntPointer *MyInt

func main() {
    var x MyInt = 10
    var p MyIntPointer = &x
    fmt.Println(*p)
}

上記のコードでは、MyIntへのポインタであるMyIntPointerという新しい型を定義しています。そして、MyIntPointerを他のポインタ型と同様に使用することができます。

構造体へのポインタ

構造体へのポインタを使用すると、関数の引数として渡すときに大きな構造体をコピーする必要がなくなります。

package main

import "fmt"

type MyStruct struct {
    x int
    y int
}

func updateStruct(s *MyStruct) {
    s.x++
    s.y++
}

func main() {
    s := MyStruct{x: 10, y: 20}
    updateStruct(&s)
    fmt.Println(s) 
}

上記のコードでは、updateStruct関数にsへのポインタを渡しています。これにより、構造体全体をコピーする必要がなくなり、大きな構造体の場合に効率的です。

ポインタは、Goの軽量な実行スレッドであるゴルーチン間でデータを共有するために使用されることもあります。ポインタをゴルーチンに渡すと、複数のゴルーチンから同じデータにアクセスし、変更することができます。

package main

import (
    "fmt"
    "sync"
)

type MyStruct struct {
    x int
    y int
}

func updateStruct(s *MyStruct, wg *sync.WaitGroup) {
    s.x++
    s.y++
    wg.Done()
}

func main() {
    s := MyStruct{x: 10, y: 20}
    var wg sync.WaitGroup

    wg.Add(2)
    go updateStruct(&s, &wg)
    go updateStruct(&s, &wg)
    wg.Wait()

    fmt.Println(s)
}

上記のコードでは、xとyという2つのフィールドを持つMyStruct構造体を作成し、構造体へのポインタを使用してxとyを増分するゴルーチンを2つ作成します。sync.WaitGroupを使用して、両方のゴルーチンが終了したことを確認してから、最終的なsの値を出力しています。

各ゴルーチンにsとsync.WaitGroupへのポインタを渡していることに注意してください。これによって、各ゴルーチンは同じデータにアクセスし、変更することができます。もし、ポインタの代わりにsとsync.WaitGroup の値を渡していたら、ゴルーチンはデータのコピーで動作し、元の値は変更されません。

Goにはガベージコレクタがあり、不要になったメモリを自動的に解放してくれることです。C言語などのように、使い終わったら手動でメモリを解放する心配がありません。

この記事が、Goでポインタがどのように機能するかを理解する助けになったら幸いです。

Go言語でSOLID原則

SOLID原則とは

SOLID原則とは、OOPにおいて、メンテナンス、拡張しやすいシステムを設計・構築するための原則のことです。 アメリカのソフトウェアエンジニアである、ロバート C. マーティンが提唱していた多くの設計原則の中からチョイスされた5つの原則の頭文字をとって命名された原則となります。

  • S :単一責任原則
  • O : オープン/クローズの原理
  • L : リスコフ置換原理
  • I : インターフェース分離の原理
  • D : 依存関係逆転の原理

単一責任原則

変更するための理由が、一つのクラスに対して一つ以上あってはならない

package main

import (
    "fmt"
    "math"
)

type circle struct {
    radius float32
}

func (c circle) area() {
    fmt.Printf("circle area: %f\n", math.Pi*c.radius*c.radius)
}

type square struct {
    sideLen float32
}

func (s square) area() {
    fmt.Printf("circle area: %f\n", s.sideLen*s.sideLen)
}

func main() {
    c := circle{radius: 5}
    c.area()

    s := square{sideLen: 2}
    s.area()
}

上記のコードは「単一責任の原則」を破っている箇所があります。 areaメソッドのfmt.Printf("circle area: %f\n",....この箇所です。 このコードは面積を計算し、その結果を表示しています。複数の責任があることがわかります。

それでは「単一責任の原則」を適用したいと思います。 メソッドを修正して、1つの責任(この場合は面積の計算)だけを行うように変更します。

package main

import (
    "math"
)

type circle struct {
    radius float32
}

func (c circle) area() float32 {
    return math.Pi * c.radius * c.radius
}

type square struct {
    sideLen float32
}

func (s square) area() float32 {
    return s.sideLen * s.sideLen
}

出力を処理する新しい型を定義します。そして、文字列の変換を新しい型とoutPrinterというメソッドに委ねます。

import (
    "fmt"
    "math"
)

type shape interface {
    area() float32
}

type outPrinter struct {}

func (op outPrinter) toText(s shape) string {
    return fmt.Sprintf("the area is: %f", s.area())
}

shapeインターフェースを引数として渡してますが、これはインターフェースのメソッドを実装している任意の型を(この場合、areaメソッドを定義しているsquareとcircle型)使用するのに役立ちます。

func main() {
    c := circle{radius: 5}
    c.area()

    s := square{sideLen: 2}
    s.area()

    out := outPrinter{}
    fmt.Println(out.toText(c))
    fmt.Println(out.toText(s))
}

mainでは次にように呼び出すことができます。outPrinter型は、必要な文字列出力を生成するために必要なすべての機能を定義します。

オープン/クローズの原理

package main

import (
    "fmt"
    "math"
)

type circle struct {
    radius float32
}

type square struct {
    sideLen float32
}

type calculator struct {
    total float32
}

func (c calculator) sumAreas(shapes ...interface{}) float32 {
    var sum float32

    for _, shape := range shapes {
        switch shape.(type) {
        case circle:
            r := shape.(circle).radius
            sum += math.Pi * r * r
        case square:
            l := shape.(square).sideLen
            sum += l * l
        }
    }

    return sum
}

func main() {
    c := circle{radius: 5}
    s := square{sideLen: 2}
    calc := calculator{}
    fmt.Println("total of areas:", calc.sumAreas(c, s))
}

上記のコードでは、「オープン/クローズの原理」を守れていません。 理由は、電卓型はsumAreasメソッドを持っていて、引数としてinterface{}shapesを定義しています。というのも、例えば新しshapeであるtriangleを定義するときにsumAreasメソッドを新しいタイプを処理するように修正する必要があるからです。 コードを改善するために、下記のアプローチをとりたいと思います。 - shapeインターフェースは、メソッド領域で定義 - サンプルの各型は、areaというメソッドを定義

type shape interface {
    area() float32
}

type circle struct {
    radius float32
}

func (c circle) area() float32 {
    return math.Pi * c.radius * c.radius
}

type square struct {
    sideLen float32
}

func (s square) area() float32 {
    return s.sideLen * s.sideLen
}

type triangle struct {
    height float32
    base   float32
}

func (t triangle) area() float32 {
    return t.base * t.height / 2
}

他の必要な変更は、電卓のsumAreasメソッドで、今回、引数はshapeとして定義され、swith文は削除され、代わりにコードは各shapeからareaメソッドを実行するようになりました。

type calculator struct {
    total float32
}

func (c *calculator) sumAreas(shapes ...shape) float32 {
    var sum float32

    for _, shape := range shapes {
        sum += shape.area()
    }
    return sum
}

func main() {
    c := circle{radius: 5}
    s := square{sideLen: 2}
    t := triangle{height: 10, base: 5}

    calc := calculator{}
    fmt.Println(calc.sumAreas(c, s, t))
}

リスコフ置換原理

S が T の派生型であれば、プログラム内で T 型のオブジェクトが使われている箇所は全て S 型のオブジェクトで置換可能

Goには継承はありませんが、コンポジションはあります。ケースによりますが、上手く「リスコフ置換原理」適用できるケースもあります。

package main

import "fmt"

type transport interface {
    getName() string
}

type vehicle struct {
    name string
}

func (c vehicle) getName() string {
    return c.name
}

type car struct {
    vehicle
    wheel int
    gates int
}

type motorcycle struct {
    vehicle
    wheel int
}

type printer struct {}

func (printer) printTransportName(p transport) {
    fmt.Println("Name: ", p.getName())
}

上記のコードでは、次のように解釈することが可能かと思います。 - このコードでは、インターフェースgetNameを定義するvehicle型を定義しています。 - 「車」と「オートバイ」はコンポジションを使用して「車両」にアクセスできます。つまり、メンバ変数にアクセスする以外に、車とオートバイは「車両」メソッドにアクセスできます。 - プリンターは、車両、車、オートバイに使用できます。 車とオートバイは乗り物として置き換えることができるため、「リスコフ置換原理」適用されてると言える。

インターフェース分離の原理

クライアントに、クライアントが利用しないメソッドへの依存を強制してはならない

package main

import "math"

type shape interface {
    area() float64
    volume() float64
}

type square struct {
    sideLen float64
}

func (s square) area() float64 {
    return s.sideLen * s.sideLen
}

func (s square) volume() float64 {
    return 0
}

type cube struct {
    sideLen float64
}

func (c cube) area() float64 {
    return math.Pow(c.sideLen, 2)
}

func (c cube) volume() float64 {
    return math.Pow(c.sideLen, 3)
}

上記のコードは、2つのメソッド(面積と体積)をもつインターフェースが定義され,正方形と立方体の2つの図形を定義し、それぞれにarea()volume()というメソッドを実装しています。

func areaSum(shapes ...shape) float64 {
    var sum float64
    for _, s := range shapes {
        sum += s.area()
    }
    return sum
}

func areaVolumeSum(shapes ...shape) float64 {
    var sum float64
    for _, s := range shapes {
        sum += s.area() + s.volume()
    }
    return sum
}

上記コードには、2つの関数があります。面積の合計と体積の合計を計算する関数です。 コードは正常に動作しますが、「クライアントは自分が使わないメソッドに依存することを強いられるべきではない」という原則から考えてみると、次のような点が浮き彫りになるかと思います。 - 正方形は平らな形状なので体積法は必要ない - オブジェクト形状であるため、立方体のみボリュームメソッドを必要とする 以上の点を踏まえてコードを次のように修正してみます。 - area()メソッドを定義するためにshapeという名前の新しいインターフェイスを追加する。 - volume()メソッドを定義するためのobjectsインターフェースを追加し、area()メソッドを使用できるようにshapeインターフェースと合成する。

type shape interface {
    area() float64
}

type object interface {
    shape
    volume() float64
}

type square struct {
    sideLen float64
}

func (s square) area() float64 {
    return math.Pow(s.sideLen, 2)
}

type cube struct {
    square
}

func (c cube) volume() float64 {
    return math.Pow(c.sideLen, 3)
}

上記のコードでは、squareはarea()メソッドを、cubeはarea()メソッドとvolume()メソッドを実装しており、「インターフェース分離の原則」に従っています。

依存関係逆転の原理

上位のモジュールは下位のモジュールに依存してはならない。どちらのモジュールも「抽象」に依存すべきである。「抽象」は実装の詳細に依存してはならない。実装の詳細が「抽象」に依存すべきである

おそらくコードで例を挙げたほうがわかりやすいかと思いますので、コードを見ながら説明したいと思います。

type MySQL struct {
    //
}

func (db MySQL) QuerySomeData() []string {
    return []string{"inf1", "inf2", "inf3"}
}

type MyRepository struct {
    db MySQL
}

func (r MyRepository) GetData() []string {
    return r.db.QuerySomeData()
}

func main() {
    mysqlDB := MySQL{}
    repo := MyRepository{db: mysqlDB}
    fmt.Println(repo.GetData())
}

上記のコードを、「依存関係逆転の原理」に当てはめて、コードを修正すると以下のようになります。 - データベースのメソッドを定義する新しいインターフェースを追加 - リポジトリを変更し、代わりにタイプ MySQLのメンバ変数を定義することで、インターフェイスである抽象化を定義

原則に基づいて適用したコードはこちらとなります。

type DBConn interface {
    QuerySomeData() []string
}
type MySQL struct {
    //
}

func (db MySQL) QuerySomeData() []string {
    return []string{"inf1", "inf2", "inf3"}
}

type MyRepository struct {
    db DBConn
}

func (r MyRepository) GetData() []string {
    return r.db.QuerySomeData()
}

func main() {
    mysqlDB := MySQL{}
    repo := MyRepository{db: mysqlDB}
    fmt.Println(repo.GetData())
}

まとめ SOLIDの原則を適用することで、アプリケーションの基礎を正しく定義しているため、より強固でスケーラブル、そして変更に耐えられるコードを書くことができるようになります。

オブジェクト指向とオブジェクト指向エクササイズ

オブジェクト指向プログラミング

プログラム全体がオブジェクトだけで設計されます。オブジェクトが持つ状態と挙動のセットを定義し、設計図とインターフェースを使って抽象化し、オブジェクト間で状態と挙動のやりとりを行い、抽象化したものを再利用したり合成することで、コードの再利用性を高めることに焦点を当てています。

クラス

クラスの役割はインスタンスの作成と再利用の単位の2つがある

カプセル化

あらゆるものの詳細を隠蔽することをカプセル化という。詳細を隠すことにより、外部に公開されたインターフェイスだけを使ってプログラムすること。

継承

継承とは、一方のクラスが他方のクラスの既存の定義を使用して自分自身を定義できるようにする、2 つのクラス間の関係のことを指します。継承を使うことによって、以前に作成した他のクラスを再利用して新しいクラスを作成することができます。 継承では、データ構造の一般化を行い、ほとんどの場合、is-a 関係に従うことになります。

ポリモーフィズム

抽象クラスやインターフェイス経由でオブジェクトに指示すると、そのオブジェクトのクラスの実装に応じて異なった動作が行われること。多態性という。ポリモーフィズムは呼び出す側のロジックを一本化する仕組み。既存コードを修正することなく機能追加できる。 抽象に対してプログラムするので、具象に依存せず、クラス間のつながりを疎結合にでき、柔軟性が向上する

インターフェース

メソッドの実装を定義せずに、クラスが実装する必要があるメソッドを指定するための仕組み。インスタンスを生成とメソッドの実装は定義しない。メソッドの実装の定義を強制できる。 定数と抽象メソッドのみを定義できる。インターフェースを実装したクラスでは、すべてのメソッドの処理を定義する必要がある。インターフェースは複数実装できる。

オーバーロード

クラス内に同じ名前で引数の型や数が違うメソッドを複数定義することをオーバーロードという。 呼び出すメソッドは名前と引数の組み合わせで決めている。 メリットは、メソッド名が変わらないので使用する側が使いやすい。出力メソッドなどもオーバーロードされて提供されている。(オーバーロードが無い言語もある、Goは無い)

オーバーライド

サブクラスでスーパークラスのメソッドを再定義することをオーバーライドという。親クラスのメソッドを子クラスで使用するときに上書きして使用できる。条件として、メソッド名、引数の型と数、戻り値は親クラスと同じでないといけない。

抽象クラス

実装内容を持たないメソッドを抽象メソッドという。抽象メソッドを持つクラスを抽象クラスという。抽象クラスはインスタンス化はできない。

コンストラク

オブジェクトの初期化のために使われる特殊なメソッドをコンストラクタという。コンストラクタを戻り値を持たない。コンストラクタを定義していない場合は、自動でデフォルトコンストラクタ(引数・処理なし)が生成される。 オブジェクトの初期状態がどのように構築されるべきか(コンストラクタ)を定義します。

static

クラスを初期化した場合は、基本的には別々のインスタンスとなり、別々の値を持つことになる。しかし、各インスタンスで共有したい情報も出てくる可能性がある。その場合に、静的フィールド(static)を使用する。 メンバ変数がインスタンスごとではなく、クラスごとに用意される。なので、静的フィールドは、クラス変数と呼ばれる。

オブジェクト指向エクササイズ

オブジェクト指向のコツはいくつかの変数をまとめて名前をつけて抽象化することだと考えています。

メソッド内のインデントは1段まで

なぜ: 複雑すぎるメソッドは凝集度が低い どうする:メソッドを分割する

// インデント1段なのでOK
func exampleFunction() {
    for i := 0; i < 10; i++ {
        fmt.Println(i)
    }
}

// インデント2段なのでNG
func exampleFunction() {
    for i := 0; i < 10; i++ {
        if i%2 == 0 {
            fmt.Println(i)
        }
    }
}

// インデント2段なのでNGなので、2段目の処理を外に出して別の関数として定義する
func exampleFunction() {
    for i := 0; i < 10; i++ {
        ifFunction(i)
    }
}

func ifFunction(i int) {
    if i%2 == 0 {
        fmt.Println(i)
    }
}

else禁止

なぜ:複雑すぎるメソッドは凝集度が低い。if-elseは、拡張されるにつれて肥大化する どうする:ガード節(ある条件を満たしていない時にリターンするか例外を投げる)早期リターン、ポリモーフィズムを使用する

// elseを使用してるのでNG
func exampleFunction(someCondition bool) {
    if someCondition {
        doSomething()
    } else {
        doSomethinsElse()
    }
}

// 早期リターンなどを使用してelse句を無くす
func exampleFunction(someCondition bool) {
    if someCondition {
        doSomething()
        return
    }
    doSomethinsElse()
}

プリミティブ型と文字列型はラップする

なぜ:ラップすることによって値に対して型を定義することで、コードの可読性を高め、変更に強く、拡張しやすくなる。値オブジェクトと呼ばれるテクニックで以下の特徴を持つ。イミュータブルオブジェクト、変更可能、振る舞いに副作用がない どうする:格納するにようを表すクラスを抽出する

// プリミティブ型を指定してるのでNG
type User struct {
    name string
    age  int
}

// プリミティブ型をラップしてるのでOK
type User struct {
    name Name
    age  Age
}

type Age struct {
    value int
}

// このようにAgeクラスでバリデートして初期化することで、Userクラスが持つageに負の値が混入することがなくなる
func (a *Age) newAge(value int) *Age {
    if value < 0 {
        panic("マイナスの年齢はだめです!!")
    }
    return &Age{value: value}
}

type Name struct {
    value string
}

名前は省略しない

コードを他の開発者が読む時に、可読性に欠けるから。

1行につきドットは一つまで

なぜ: オブジェクトの内部構造が見える。依存するオブジェクトの種類が増える どうする:委譲メソッドを作る

// メソッドチェインはドットが2つ以上はNG
firstName := user.getName().split("")[0]

1クラスは50行、1ディレクトリ10ファイルまで

なぜ: 大きいクラス、パッケージは、責務を過剰に負ってしまっているから どうする:1つの責務に分割するべし

1つのクラスにつき、インスタンス変数は2つまで

なぜ: 多くのメンバ変数を持つクラスは凝集度が低い どうする:クラスを分割する

// メンバ変数が2つを超えたのでNG
type User struct {
    name        Name
    phoneNumber PhoneNumber
    email       Email
}

// メンバ変数が2つを超えた場合は、分割する
type User struct {
    name    Name
    contact Contact
}

type Contact struct {
    phoneNumber PhoneNumber
    email       Email
}

ファーストクラスコレクションを使用する

なぜ: コレクション(リスト、マップなど)をラップしたクラスのこと。コレクションは、いわばプリミティブ型のようなもの。 どうする:コレクションをメンバ変数に持つクラスを作る

// NG
var books []Book = getBooks()

// OK
var bookShelf BookShelf = getBookShelf()

type BookShelf struct {
    books []Book
}

Getter,Setter,publicプロパティの禁止

なぜ: カプセル化が破られる。カプセル化したメソッドは、外部からの不当な攻撃を防ぐ必要があるので使用しない。 どうする:尋ねるな、命じろ!。情報をゲットして何かするのではなく、してほしいことを依頼する。Getterを呼びたくなったときは、getした後で何をするのかを考えて、その処理を呼び出し元に返してあげる。

// NG
var book Book = dao.findBook(bookId)
totalPrice := book.getPrice() * quantity

// OK
var book Book = dao.findBook(bookId)
totalPrice := book.calculateTotal(quantity)

OKとNGで何が変わるのか? ・Bookの内部情報がより隠される=>前者では少なくとも「本の単価」についてが外部に引き出されていました。しかし後者ではより本来の目的に近い「合計額」のみが引き出されています ・処理の重複が起きにくくなる=>このように情報の本来の持ち主がデータの加工を担当すると、あちらこちらで同じような加工処理が行われることがなくなります。つまり全体としてのコード量や複雑さが減り、メンテナンス性が向上します ・変更に強くなる=>1つ目や2つ目と強く関係しますが、合計額算出の具体的な処理の内容はBookの中に隠蔽されています。従って、算出方法が変わったとしてもBook#calcurateTotalが変更されるだけで済みます

「getter, setterを使うな」というのはより正しく言うならば、「そのgetter, setter呼び出しは呼び出される側への責務の割り当てに置換できないか検討しろ」ということになります 今回の内容を1行ずつにまとめると ・データの加工は、そのデータの持ち主がやるのがベスト。その結果、カプセル化が進み、コードの重複を排除できる ・カプセル化の目的は、変更による影響範囲を極力狭くすること

Tell, Don't Ask

「Tell, Don't Ask.」とは、オブジェクト指向プログラミングにおいて"良い"とされる考え方のひとつ。 日本語だと大体「求めるな、命じよ」と訳されることが多い。 もうちょっと具体的にすると、ある処理をする際、その処理に必要な情報をオブジェクトから引き出さないで、情報を持ったオブジェクトにその処理をさせろということ。

getterというのはまさにオブジェクトから情報を引き出すメソッドである。 つまり、あるクラスで他クラスのgetterを呼び出すような処理を実装している場合、その処理は本来呼び出されるクラス側で実装されるべきだということ。 setterも同様に、フィールドの中身を変えるような処理はそもそもそのフィールドをもつクラス内で完結させるべきである、という考え方。

代表的なアルゴリズムをGoで書いてみた

代表的なアルゴリズムをGoを使って学習していた時に、自身のメモとして記述していた内容を改良して記事にしてみました。 Pythonなどで書いている記事はそれなりにあると思いますが、Goで書いている記事は少ない印象でしたので誰かの参考になれば幸いです!!

linear search(リニアサーチ)

線型探索は、検索のアルゴリズムの一つ。 リストや配列に入ったデータに対する検索を行うにあたって、 先頭から順に比較を行い、それが見つかれば終了する。 n個のデータからm個のデータを検索する場合、時間計算量はO(nm)、空間計算量はO(1)である。

func linearSearch(nums []int, value int) int {
    for i, num := range nums {
        if num == value {
            return i
        }
    }
    return -1
}

binary search(バイナリサーチ

イナリサーチとは、ソート済み配列に対する探索アルゴリズムの一つ。

ソート済みのリストや配列に入ったデータに対する検索を行うにあたって、 中央の値を見て、検索したい値との大小関係を用いて、検索したい値が中央の値の右にあるか、左にあるかを判断して、片側には存在しないことを確かめながら検索していきます。 大小関係を用いるため、ソートされていないリストや大小関係の定義されない要素を含むリストには二分探索を用いることはできません。

n個のデータがある場合、時間計算量はO(logn) n個のデータの中央の値を見ることで、1回の操作でn/2個程度(奇数の場合は(n-1)/2個、偶数の場合はn/2個または(n/2)-1個)の要素を無視することができます。

func binarySearch(nums []int, value int) int {
    low := 0
    high := len(nums) - 1

    for low <= high {
        mid := (low + high) / 2

        if nums[mid] == value {
            return mid
        }
        if nums[mid] < value {
            low = mid + 1
        } else {
            high = mid - 1
        }
    }
    return -1
}

// 再帰を使った、バイナリサーチ
func binarySearchRecursive(nums []int, value int, low, high int) int {
    if low > high {
        return -1
    }
    mid := (low + high) / 2
    if nums[mid] == value {
        return mid
    } else if nums[mid] < value {
        return binarySearchRecursive(nums, value, mid+1, high)
    } else {
        return binarySearchRecursive(nums, value, low, mid-1)
    }
}

bubble sort(バブルソート

隣り合う要素の大小を比較しながら整列させるソートアルゴリズム

アルゴリズムが単純で実装も容易である一方、最悪時間計算量は O(n2) と遅いため、一般にはマージソートヒープソートなど、より最悪時間計算量の高速な方法が利用されるます。

アルゴリズム

要素の1番目と2番目を比較し、順番が逆であれば入れ換える。次に2番目と3番目を比較して入れ換える。これを最後まで行うと、最後の数だけが最小または最大の数として確定するので、確定していない部分について1つずつ減らしながら繰り返すアルゴリズムとなります。

func bubbleSort(nums []int) []int {
    lenNums := len(nums)

    for i := 0; i < lenNums; i++ {
        for j := 0; j < lenNums-1-i; j++ {
            if nums[j] > nums[j+1] {
                nums[j], nums[j+1] = nums[j+1], nums[j]
            }
        }
    }
    return nums
}

selection sort(選択ソート)

ソートのアルゴリズムの一つ。 配列から最小値を探し、配列の先頭要素と入れ替えていくことで並べ替える。

最悪時間計算量は O(n2) と遅いため、一般にはクイックソートなどのより高速な方法が利用される。しかし、空間計算量が限られるため他の高速な手法が使えない場合や、ソートする配列が充分小さく、選択ソートが高速に動作することが保証されている場合に利用されることがある。 選択ソートは内部ソートである。また、安定ソートではない。 選択ソートの改良として、ヒープソートが挙げられる。

アルゴリズム

1.1 番目の要素から最後尾の要素までで最も値の小さいものを探し、それを 1 番目の要素と交換する(1番目の要素までソート済みとなる) 2.以降同様に、未ソート部分の最小要素を探索し、未ソート部分の先頭要素と交換する 3.すべての要素がソート済みになったら処理を終了する

func selectionSort(nums []int) []int {
    lenNums := len(nums)

    for i := 0; i < lenNums; i++ {
        minIdx := i
        for j := i + 1; j < lenNums; j++ {
            if nums[minIdx] > nums[j] {
                minIdx = j
            }
        }
        nums[i], nums[minIdx] = nums[minIdx], nums[i]
    }
    return nums
}

insertion sort(挿入ソート)

整列してある配列に追加要素を適切な場所に挿入する。 時間計算量は平均・最悪ケースでともに Ο(n2) であり、クイックソートマージソートなどと比べれば遅い。 - アルゴリズムが単純で実装が容易 - 小さな配列に対しては高速 - 安定 などの特徴から利用されることがあります。挿入ソートを高速化したソート法として、シェルソートが有名です。

アルゴリズム

まず0番目と1番目の要素を比較し、順番が逆であれば入れ換える。次に、2番目の要素が1番目までの要素より小さい場合、正しい順に並ぶように「挿入」する(配列の場合、前の要素を後ろに一つずつずらす)。この操作で、2番目までのデータが整列済みとなる(ただし、さらにデータが挿入される可能性があるので確定ではない)。このあと、3番目以降の要素について、整列済みデータとの比較と適切な位置への挿入を繰り返す。

func insertionSort(nums []int) []int {
    lenNums := len(nums)

    for i := 1; i < lenNums; i++ {
        temp := nums[i]
        j := i - 1
        for j >= 0 && nums[j] > temp {
            nums[j+1] = nums[j]
            j -= 1
        }
        nums[j+1] = temp
    }
    return nums
}

shell sort

配列の中である程度間隔が離れた要素の組ごとに挿入ソートを行い、間隔を小さくしながら同様のソートを繰り返すことで高速化するアルゴリズムになります。

アルゴリズム

アルゴリズムの基本は挿入ソートと同じ。挿入ソートは「ほとんど整列されたデータに対しては高速」という長所を持つが、隣接した要素同士しか比較・交換を行わないため、あまり整列されていないデータに対しては低速となります。 シェルソートは、「飛び飛びの列を繰り返しソートして、配列を大まかに整列された状態に近づけていく」ことにより、挿入ソートの長所を活かした形のアルゴリズムになります。 アルゴリズムは次の通りです。 1.適当な間隔h を決める(hの決め方については後述) 2.間隔h をあけて取り出したデータ列に挿入ソートを適用する 3.間隔h を狭めて、2.を適用する操作を繰り返す 4.h=1 になったら、最後に挿入ソートを適用して終了

func shellSort(nums []int) []int {
    lenNums := len(nums)
    gap := lenNums / 2

    for gap > 0 {
        for i := gap; i < lenNums; i++ {
            temp := nums[i]
            j := i
            for j >= gap && nums[j-gap] > temp {
                nums[j] = nums[j-gap]
                j -= gap
            }
            nums[j] = temp
        }
        gap /= 2
    }
    return nums
}

Quick sort

n 個のデータをソートする際の最良計算量および平均計算量は、O(n log n)になります。 他のソート法と比べて一般的に最も高速だと言われているいますが、対象のデータの並びやデータの数によっては必ずしも速いわけではなく、最悪の計算量はO(n2)になることもあります。

アルゴリズム

1.ピボットの選択:適当な値(ピボット(英語版)という)を境界値として選択する 2.配列の分割:ピボット未満の要素を配列の先頭側に集め、ピボット未満の要素のみを含む区間とそれ以外に分割する 3.再帰:分割された区間に対し、再びピボットの選択と分割を行う 4.ソート終了:分割区間が整列済みなら再帰を打ち切る

func partition(nums []int, low, high int) int {
    i := low - 1
    pivot := nums[high]
    for j := low; j < high; j++ {
        if nums[j] <= pivot {
            i++
            nums[i], nums[j] = nums[j], nums[i]
        }

    }
    nums[i+1], nums[high] = nums[high], nums[i+1]

    return i + 1
}

func quickSort(nums []int, low, high int) []int {
    if low < high {
        partitionIndex := partition(nums, low, high)
        quickSort(nums, low, partitionIndex-1)
        quickSort(nums, partitionIndex+1, high)
    }
    return nums
}

func quickSortStart(nums []int) []int {
    return quickSort(nums, 0, len(nums)-1)
}

merge sort

マージソートは、ソートのアルゴリズムで、既に整列してある複数個の列を1個の列にマージする際に、小さいものから先に新しい列に並べれば、新しい列も整列されているというボトムアップの分割統治法。大きい列を多数の列に分割し、そのそれぞれをマージする作業は並列化できます。 n個のデータを含む配列をソートする場合、最悪計算量O(n log n)になります。分割と統合の実装にもよるが、一般に安定なソートを実装できる。 クイックソートと比べると、最悪計算量は少ない。ランダムなデータでは通常、クイックソートのほうが速い。

アルゴリズム

1.データ列を分割する(通常、二等分する) 2.分割された各データ列で、含まれるデータが1個ならそれを返し、2個以上ならステップ1から3を再帰的に適用してマージソートする 3.二つのソートされたデータ列(1個であればそれ自身)をマージする

func mergeSort(nums []int) []int {
    var lenNums = len(nums)

    if lenNums == 1 {
        return nums
    }

    mid := lenNums / 2
    var (
        left  = make([]int, mid)
        right = make([]int, lenNums-mid)
    )
    for i := 0; i < lenNums; i++ {
        if i < mid {
            left[i] = nums[i]
        } else {
            right[i-mid] = nums[i]
        }
    }

    return merge(mergeSort(left), mergeSort(right))
}

func merge(left, right []int) (result []int) {
    result = make([]int, len(left)+len(right))

    i := 0
    for len(left) > 0 && len(right) > 0 {
        if left[0] < right[0] {
            result[i] = left[0]
            left = left[1:]
        } else {
            result[i] = right[0]
            right = right[1:]
        }
        i++
    }

    for j := 0; j < len(left); j++ {
        result[i] = left[j]
        i++
    }
    for j := 0; j < len(right); j++ {
        result[i] = right[j]
        i++
    }
    return
}

まとめ

まだまだ、たくさんのアルゴリズムがあるので私自身も色々学習したいと思っております。 アルゴリズムやデータ構造って、へぇーこんなロジックでできているんだなど色々と面白いと思いますので、学習してみてはいかがでしょうか。 データ構造を学習した時の、メモもあるので改良してまた記事として出そうと思います。

Goを使ってS3にファイルを一括アップロードする

Amazon S3

Amazon S3は、ウェブ上のどこからでもファイルを保存し、取り出すことができるAmazonのストレージです。Amazonが提供する、拡張性、信頼性、高速性、安価性に優れたストレージになります。ファイルの保存と取り出しのための使いやすい開発キットを提供されています。

今回は、S3のバケットgolangを使って、大量のファイルをアップロードしてみたいと思います。S3にファイルをアップロードするために、Go AWS SDKを使用します。(AWS SDK for Goは、Go 1.15以降のバージョンが必要です)

AWS Golang SDKをダウンロードします。

$ go get github.com/aws/aws-sdk-go/aws

main.goファイルを作成し、必要なパッケージをインポートして、S3バケットにファイルをアップロードします。

package main
import (
    "bytes"
    "fmt"
    "log"
    "net/http"
    "os"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/credentials"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/s3"
)

Session構造体を使うので、このレシーバをメインのアップロード関数に渡します。

type Session struct {
    S3Session *session.Session
}

AWSセッションをセットアップする

S3 REGIONとSECRET ID、SECRET KEYを使って設定を行い、複数のファイルをS3にアップロードするためのAWSセッションを1つ作成します。そして、upload()メソッドを呼び出し、AWSセッションインスタンスとファイルの詳細を渡し、S3サーバーにファイルをアップロードします。

func main() {
    paths := []string{"", ""}
    credential := credentials.NewStaticCredentials(
        os.Getenv("SECRET_ID"),
        os.Getenv("SECRET_KEY"),
        "",
    )
    awsConfig := aws.Config{
        Region:      aws.String(os.Getenv("REGION")),
        Credentials: credential,
    }
    s, err := session.NewSession(&awsConfig)
    if err != nil {
        log.Println("failed to create S3 session:", err.Error())
        return
    }
    se := Session{s}
    err = se.upload(paths)
    if err != nil {
        log.Println(err.Error())
        return
    }
}

まずos.Getenv()を使ってすべての設定情報を読み込み、aws sdkの関数credentials.NewStaticCredentials()に渡しています。またaws.Config構造体を呼び出してその中のcredential変数を渡し、環境変数からREGION値も読み込んでいます。

aws.Config型のawsConfigをsession.NewSession()に渡し、awsのs3セッションを返すことで、1つのセッションで複数のファイルをアップロードできるようにしています。

Session構造体にセッションを渡し、その後、uploadメソッドを呼び出して、アップロードする必要があるすべてのファイルのパスとセッション情報を渡しています。

S3にファイルを一括アップロードするためのアップロードメソッドを作成する

S3にファイルをアップロードするためのメソッドupload()を作成します。ファイルを1つずつ開いてバッファに格納し、S3からPutObject()メソッドを使用してS3にファイルを配置します。ファイルをアップロードする際に、PutObjectInputにオプションを指定します。s3.PutObjectInput構造体のServerSideEncryptionオプションを使用して、ファイルのAES256暗号化を有効にします。

func (s Session) upload(paths []string) error {
    for _, path := range paths {
        upFile, err := os.Open(path)
        if err != nil {
            log.Printf("failed %s, error: %v", path, err.Error())
            continue
        }
        defer upFile.Close()
        upFileInfo, err := upFile.Stat()
        if err != nil {
            log.Printf("failed to get stat %s, error: %v", path, err.Error())
            continue
        }
        var fileSize int64 = upFileInfo.Size()
        fileBuffer := make([]byte, fileSize)
        upFile.Read(fileBuffer)
        // uploading
        _, err = s3.New(s.S3Session).PutObject(&s3.PutObjectInput{
            Bucket:               aws.String(os.Getenv("BUCKET_NAME")),
            Key:                  aws.String(path),
            ACL:                  aws.String("public-read"), 
            Body:                 bytes.NewReader(fileBuffer),
            ContentLength:        aws.Int64(int64(fileSize)),
            ContentType:          aws.String(http.DetectContentType(fileBuffer)),
            ContentDisposition:   aws.String("attachment"),
            ServerSideEncryption: aws.String("AES256"),
            StorageClass:         aws.String("INTELLIGENT_TIERING"),
        })
        if err != nil {
            log.Printf("failed to upload %s, error: %v", path, err.Error())
            continue
        }
        url := "https://%s.s3-%s.amazonaws.com/%s"
        url = fmt.Sprintf(url, os.Getenv("BUCKET_NAME"), os.Getenv("REGION"), path)
        fmt.Printf("Uploaded File Url %s\n", url)
    }
    return nil
}

Sessionをレシーバとするupload()というメソッドを作成しました。Session構造体はmain関数で作成したawsセッションで、upload()メソッドにはアップロードする必要があるすべてのファイルも渡しています。

upload()メソッドでは、path変数にループをかけ、ループ内でまずos.Openを呼び出し、ファイルを開いた後にStatメソッドを呼び出し、aws s3 sdkメソッドに渡す必要がある、開いたファイルに関するすべての情報を取得しています。

s3.PutObjectInputに渡す情報は、パス、ファイルサイズ、ファイルバッファのみで、s3バケットにファイルをアップロードするための有効なhttpリクエストを作成するために使用することが可能となっています。

Goで、json.UnmarshalとMarshalとエンコード

Golangの、UnmarshalとMarshalについて解説してみたいと思います。 ネットワーク越しで、取ってきたデータをGoの構造体に変換したり、Goの構造体からJSONに変換してデータを送信したりと使用する機会も多いと思うので、是非参考にしていただければと思います。

Unmarshal

Unmarshalは、ネットワーク越しに送信されたデータをGoの構造体に変換します。

func Unmarshal(data []byte, v any) error

Unmarshalは、JSON形式で受け取った値を指定した構造体に格納することができます。第1引数にJSON形式のデータを、第2引数に格納したい構造体を指定します。 第2引数の値がnilもしくはポインタではない場合、InvalidUnmarshalErrorを返します。

package main

import (
    "encoding/json"
    "fmt"
)

type Book struct {
    Title     string
    Author    string
    Publisher string
}

func main() {
    b := []byte(`{"title": "リーダブルコード", "author": "Trevor Foucher", "Publisher": "OREILLY"}`)
    var book Book
    if err := json.Unmarshal(b, &book); err != nil {
        fmt.Println(err)
    }
    fmt.Println(book.Title, book.Author, book.Publisher)
}

実行してみます。

$ docker-compose up
go_1  | running...
go_1  | リーダブルコード Trevor Foucher OREILLY

ネットワークで入ってきた、byteのスライスをBook構造体のキーをみて変換してくれるのが、Unmarshalとなります。

Marshal

Goの構造体に格納したデータを、JSON形式に変換してネットワーク越しに送信したい場合に、Marshalを使用します。

func Marshal(v any) ([]byte, error)

Marshalは、引数の値をJSON形式にエンコーディングして返します。

package main

import (
    "encoding/json"
    "fmt"
)

type Book struct {
    Title     string
    Author    string
    Publisher string
}

func main() {
    b := []byte(`{"title": "リーダブルコード", "author": "Trevor Foucher", "Publisher": "OREILLY"}`)
    var book Book
 
    v, err := json.Marshal(book)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(string(v))
}

実行してみます。

$ docker-compose up
go_1  | running...
go_1  | {"Title":"リーダブルコード","Author":"Trevor Foucher","Publisher":"OREILLY"}

JSON形式の値が返って来ていることが確認できます。

Marshalを使ってJSON形式で出力をしましたが、キーの名前が大文字になっています。JSON形式でデータを扱う場合、大文字になることはあまり無いため小文字にしてみましょう。小文字で指定をするには、構造体にタグを付けることで対応できます。 Marshalするときには、どのような名前でエンコードするかを指定することができます。

type Book struct {
    Title     string `json:"title"`
    Author    string `json:"author"`
    Publisher string `json:"publisher"`
}

再度実行してみます。

$ docker-compose up
go_1  | running...
go_1  | {"title":"リーダブルコード","author":"Trevor Foucher","publisher":"OREILLY"}

構造体の頭文字は大文字ですが、タグをつけることでキーが小文字になっていることが確認できます。

データを隠す

このデータは表示させたくないというケースがあると思います。そのような場合は、JSONのタグにハイフンを指定することで、データを隠すことができます。

type Book struct {
    Title     string `json:"title"`
    Author    string `json:"-"`
    Publisher string `json:"publisher"`
}

実行してみます。

$ docker-compose up
go_1  | running...
go_1  | {"title":"リーダブルコード","publisher":"OREILLY"}

ハイフンを指定した、Authorが表示されていないのが確認できると思います。 著者名を隠すってどんなケースだよ!ですよね笑、題材が悪かったですね笑 パスワードとかkeyとかで使えるのかなと思います!

Marshalを独自カスタマイズ

Marshalを拡張させて、独自に処理を加えてカスタマイズすることもできます。便利ですね! MarshalJSONの形で記述しないとカスタマイズすることができないです。Marshalが呼ばれた時に自動的にMarshalJSONメソッドが呼ばれる様になります。

package main

import (
    "encoding/json"
    "fmt"
)

type Book struct {
    Title     string `json:"title"`
    Author    string `json:"-"`
    Publisher string `json:"publisher"`
}

func (b Book) MarshalJSON() ([]byte, error) {
    v, err := json.Marshal(&struct {
        Publisher string
    }{
        Publisher: b.Publisher + "Japan",
    })
    return v, err
}

func main() {
    b := []byte(`{"title": "リーダブルコード", "author": "Trevor Foucher", "Publisher": "OREILLY"}`)
    var book Book
    if err := json.Unmarshal(b, &book); err != nil {
        fmt.Println(err)
    }
    fmt.Println(book.Title, book.Author, book.Publisher)

    v, err := json.Marshal(book)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(string(v))
}

実行してみます。

$ docker-compose up
go_1  | running...
go_1  | {"Publisher":"OREILLY Japan"}

OREILLYから、OREILLY Japanに変わってると思います。

Unmarshalを独自カスタマイズ

Marshalと同様にUnmarshalもUnmarshalJSONを使用すれば、カスタマイズできます。

package main

import (
    "encoding/json"
    "fmt"
)

type Book struct {
    Title     string `json:"title"`
    Author    string `json:"author"`
    Publisher string `json:"publisher"`
}

func (b *Book) UnmarshalJSON(byte []byte) error {
    type Book2 struct {
        Title string
    }
    var b2 Book2
    err := json.Unmarshal(byte, &b2)
    if err != nil {
        fmt.Println(err)
    }
    b.Title = b2.Title + "-より良いコードを書くためのシンプルで実践的なテクニック"
    return err
}

func main() {
    b := []byte(`{"title": "リーダブルコード", "author": "Trevor Foucher", "Publisher": "OREILLY"}`)
    var book Book
    if err := json.Unmarshal(b, &book); err != nil {
        fmt.Println(err)
    }
    fmt.Println(book.Title, book.Author, book.Publisher)

    v, err := json.Marshal(book)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(string(v))
}

実行してみます。

$ docker-compose up
go_1  | running...
go_1  | {"title":"リーダブルコード-より良いコードを書くためのシンプルで実践的なテクニック",

Marshalと同様しっかりカスタマイズできていますね! いかがだったでしょうか、UnmarshalとMarshalは、使用頻度も高いと思いますので、少しでも理解の助けになればと思います!

Go + ginを使って簡単なAPIを作る

Goのフレームワークである、ginを使って簡易的なAPIを作成していきたいと思います。

ginとは

ginは、Goで書かれたWebアプリケーションフレームワークになります。 高速なパフォーマンス、ミドルウェアが充実、JSONのリクエストのバリデーション、ルーティングのグループ化、エラー管理、組み込みのレンダリング、拡張性があるなどの特徴を持ちます。

インストール

$ go get -u github.com/gin-gonic/gin

コード内でインポートする

import "github.com/gin-gonic/gin"

これで、ginを使用することができます、簡単ですね!

docker環境作成

Docker環境で、行いたいと思うので、以下ファイルを作成します。説明については割愛させていただきます。 ホットリロードするために、Airを導入しています。

FROM golang:latest

WORKDIR /go/src

COPY ./ .

RUN go install github.com/cosmtrek/air@latest

CMD ["air", "-c", ".air.toml"]
version: '3'
services:
  go:
    build:
      context: .
    volumes:
      - ./:/go/src
    ports:
      - "8080:8080"
# Config file for [Air](https://github.com/cosmtrek/air) in TOML format

# Working directory
# . or absolute path, please note that the directories following must be under root.
root = "."
tmp_dir = "tmp"

[build]
# Just plain old shell command. You could use `make` as well.
cmd = "go build -o ./tmp/main ."
# Binary file yields from `cmd`.
bin = "tmp/main"
# Customize binary, can setup environment variables when run your app.
full_bin = "APP_ENV=dev APP_USER=air ./tmp/main"
# Watch these filename extensions.
include_ext = ["go", "tpl", "tmpl", "html"]
# Ignore these filename extensions or directories.
exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules"]
# Watch these directories if you specified.
include_dir = []
# Exclude files.
exclude_file = []
# Exclude specific regular expressions.
exclude_regex = ["_test\\.go"]
# Exclude unchanged files.
exclude_unchanged = true
# Follow symlink for directories
follow_symlink = true
# This log file places in your tmp_dir.
log = "air.log"
# It's not necessary to trigger build each time file changes if it's too frequent.
delay = 1000 # ms
# Stop running old binary when build errors occur.
stop_on_error = true
# Send Interrupt signal before killing process (windows does not support this feature)
send_interrupt = false
# Delay after sending Interrupt signal
kill_delay = 500 # ms
# Add additional arguments when running binary (bin/full_bin). Will run './tmp/main hello world'.
args_bin = ["hello", "world"]

[log]
# Show log time
time = false

[color]
# Customize each part's color. If no color found, use the raw app log.
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"

[misc]
# Delete tmp directory on exit
clean_on_exit = true

API作成

それでは、APIの作成を行なっていきます。

package main

import (
    "errors"
    "net/http"

    "github.com/gin-gonic/gin"
)

type book struct {
    ID       string `json:"id"`
    Title    string `json:"title"`
    Author   string `json:"author"`
    Quantity int    `json:"quantity"`
}

var books = []book{
    {ID: "1", Title: "In Search of Lost Time", Author: "Marcel Proust", Quantity: 2},
    {ID: "2", Title: "The Great Gatsby", Author: "F. Scott Fitzgerald", Quantity: 5},
    {ID: "3", Title: "War and Peace", Author: "Leo Tolstoy", Quantity: 6},
}

func getBooks(c *gin.Context) {
    c.IndentedJSON(http.StatusOK, books)
}

func main() {
    router := gin.Default()

    router.GET("/books", getBooks)

    router.Run()
}

gin.Default

gin.Defaultは、*gin.Engine構造体を返します。ginでは、このEngine構造体を使って,エンドポイント、ミドルウェアなどを登録しておくことができます。

gin.Context

gin.Contextは、リクエストに関連する情報が格納されており、また応答を返すことができます。 クエリパラメータ、データ、ペイロード、ヘッダーなどが格納されていて、アクセスすることが可能です。 標準のGoで、HandleFuncを作成する際に、引数にResponseWriter*http.Requestを渡すと思いますが、Context構造体のフィールドにも存在します。 Run()メソッドも、内部では、http.ListenAndServe()を呼び出してます。 標準のGoで、サーバー立ち上げがわかる方なら、置き換わってると考えれば理解しやすいと思います。

コードは、localhost:8080/booksにアクセスすると、getBooksが呼ばれます。処理としては、book構造体のスライスであるbooksをJSONとしてシリアライズして、返しています。 動かしてみます

$ docker-compose up
go_1  | [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
go_1  | [GIN-debug] Listening and serving HTTP on :8080

立ち上がったので、curlコマンドを使用し、作成したエンドポイントにカールします。

$ curl localhost:8080/books
[
    {
        "id": "1",
        "title": "In Search of Lost Time",
        "author": "Marcel Proust",
        "quantity": 2
    },
    {
        "id": "2",
        "title": "The Great Gatsby",
        "author": "F. Scott Fitzgerald",
        "quantity": 5
    },
    {
        "id": "3",
        "title": "War and Peace",
        "author": "Leo Tolstoy",
        "quantity": 6
    }
]         

インデントされたJSONが作成され返されてますね! 次は、bookに新しく、追加できるようにしていきたいと思います。

func createBook(c *gin.Context) {
    var newBook book

    if err := c.BindJSON(&newBook); err != nil {
        return
    }

    books = append(books, newBook)
    c.IndentedJSON(http.StatusCreated, newBook)
}

func main() {
    router := gin.Default()

    router.GET("/books", getBooks)
    router.POST("/books", createBook)

    router.Run()
}

追加するための、JSONファイルを作成します。

{
  "id": "4",
  "title": "Hamlet",
  "author": "William Shakespeare",
  "quantity": 2
}

コンテナを立ち上げます。

docker-compose up
go_1  | [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
go_1  | [GIN-debug] Listening and serving HTTP on :8080

立ち上がったので、先ほど作成したJSONファイルを送信してみたと思います。

$ curl localhost:8080/books --include --header "Content-Type: application/json" -d @body.json --request "POST"

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: Sat, 24 Sep 2022 06:16:13 GMT
Content-Length: 96

{
    "id": "4",
    "title": "Hamlet",
    "author": "William Shakespeare",
    "quantity": 2
}               

追加することができたと思います。 コードは、リクエストからきたJSONを、book構造体にバインドして、booksの中にappendして、インデントされたJSONを返します。 次は、特定の本を取得してみたいと思います。

func bookById(c *gin.Context) {
    id := c.Param("id")
    book, err := getBookById(id)
    if err != nil {
        c.IndentedJSON(http.StatusNotFound, gin.H{"message": "Book not found."})
        return
    }
    c.IndentedJSON(http.StatusOK, book)
}

func getBookById(id string) (*book, error) {
    for i, b := range books {
        if b.ID == id {
            return &books[i], nil
        }
    }

    return nil, errors.New("book not found")
}

func main() {
    router := gin.Default()

    router.GET("/books", getBooks)
    router.GET("/books/:id", bookById)
    router.POST("/books", createBook)

    router.Run()
}

コンテナを立ち上げます。

$ docker-compose up
go_1  | [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
go_1  | [GIN-debug] Listening and serving HTTP on :8080

立ち上がったので、特定の本を取得してみたいと思います。

$ curl localhost:8080/books/2
{
    "id": "2",
    "title": "The Great Gatsby",
    "author": "F. Scott Fitzgerald",
    "quantity": 5
}          

指定した本を取得することができました。本が存在しない場合も試してみます。存在しないidを指定してみます。

$ curl localhost:8080/books/5
{
    "message": "Book not found."
}

"message": "Book not found."存在しないid指定した時の動作も問題無さそうですね。

以上となります。 Goは、標準のライブラリが充実してるので、そちらを使用するのも良いと思いますが、規模が大きくなるとginなどのフレームワークを使用した方が効率よく開発出来ると思いますので、触ってみてはいかがでしょうか。