Open16

初めてのGo言語

silvasilva

第3章 合成型

配列

配列の初期化は以下のようにする。

x := [3]int{10, 20, 30} //配列の長さを指定する方法

y := [...]int{10, 20, 30} //配列リテラルを使う方法

fmt.Println(x == y) //true
silvasilva

7章 メソッド

### メソッドの定義
メソッドは以下のように定義する

// 型Personを定義
type Person struct {
    LastName string
    FirstName string
    Age int
}

// Stringメソッドを定義
func (p Person) String() string {
    return fmt.Sprintf("%s %s: 年齢%d歳", p.LastName, p.firstName, p.Age)
}

型アサーション

type MyInt int

func main() {
    var i any
    var mine MyInt = 20
    i = mine
    i2 := i.(MyInt) // iをMyInt型と仮定して、その値(20)を取得
    fmt.Println(i2)  // 20 
}
silvasilva

ポインタ型レシーバと値型レシーバの使い分け

  • メソッドがレシーバを変更する・・・ ポインタレシーバ
  • メソッドがnilを扱う必要がある・・・ポインタレシーバ
  • メソッドがレシーバを変更しない・・・値レシーバ
silvasilva

第8章 エラー処理

8.5 エラーのラップ

エラーのラップとは

エラーのラップとは、エラーを保ったまま付加的な情報を追加すること。例として、エラーにコンテキストを付け加えることがある。

ラップ方法

以下のように、fmt.Errorfを用いる。

fmt.Errorf("エラー内容:  %w", err)

アンラップ方法

errors.Unwrapを用いる

  // オリジナルのエラーを作成
originalError := errors.New("original error")

 // オリジナルのエラーをラップ
wrappedError := fmt.Errorf("an error occurred: %w", originalError)

// ラップされたエラーを出力
fmt.Println(wrappedError)

// ラップされたエラーをアンラップしてオリジナルのエラーを取り出す
unwrappedError := errors.Unwrap(wrappedError)

 // アンラップしたエラーを出力
fmt.Println(unwrappedError)
silvasilva

8.6 IsAs

  • Is

errors.Is==を使うと、ラップされた各エラーと指定されたエラーを比較できる。

func fileChecker(name string) error {
	f, err := os.Open(name)
	if err != nil {
		return fmt.Errorf("in fileChecker: %w", err)
	}

	f.Close()
	return nil
}

func main() {
     // ラップされたエラー
	err := fileChecker("not_here.txt")
	if err != nil {
               // true
		if errors.Is(err, os.ErrNotExist) {
			fmt.Println("File not found")
		}
	}
}
silvasilva

ゴルーチン

用語

  • プロセス ・・・具体的なプログラムがコンピュータのOSによって実行されているもの
  • スレッド・・・実行の単位。OSから決められた時間が与えられて実行される
silvasilva

チャネル

作り方

ゴルーチンでは情報のやり取りにチャネルを使う。作り方は以下の通り。

ch := make(chan int)  // make(chan 型)

読み込み、書き込み、バッファリング

チャネルとやり取りする際には「<-」を使う。チャネルからの読み込みにはチャネル変数の左側に<-を書き、チャネルに書き込むにはチャネル変数の右側に<-を書く

a := <-ch //チャンネルからの読み込み。チャンネル変数chから値を読み込み、aに代入する
ch <- b  //チャンネルへの書き込み。bの値をチャンネル変数の右側に<-を書く

デフォルトでは、チャネルはバッファリングされない。オープンされてバッファリングされていないチャネルへの書き込みが行われるとき、書き込み側のゴルーチンは同じチャネルから他のゴルーチンが読み込みを行うまでポーズする。つまり、読み込みが終わるまで書き込みされない。
同様に、オープンされバッファリングされていないチャネルからの読み込みを行っているゴルーチンは同じチャネルへ他のゴルーチンが書き込みを行うまで停止する。

バッファリングされるチャネルの生成は以下のようにする。

ch := make(chan int, 10)  // バッファのキャパシティは10
silvasilva

for-range とチャネル

以下のようにfor-rangeを使ってチャネルからの読み込みができる。

for v := range ch { // chがクローズされるまでループ
    fmt.Println(v)
}
silvasilva

バックプレッシャ

特定のコンポーネントの仕事量を制限して、システム全体が効率よく動作するよう、バッファ付きのチャネルとselect文を使って同時リクエストの数を制限する。

package main

import (
    "fmt"
    "time"
)

func main() {
    taskCh := make(chan int, 5) // キャパシティを5に設定
    doneCh := make(chan bool)

    // タスク生成
    go func() {
        for i := 0; i < 4; i++ {
            select {
            case taskCh <- i:
                fmt.Println("タスクを生成します:", i)
            case <-doneCh:
                return
            }
            time.Sleep(1 * time.Second) // タスク生成に時間がかかると仮定
        }
        close(taskCh)
    }()

    // タスク処理
    go func() {
        for task := range taskCh {
            fmt.Println("タスクを処理します:", task)
            time.Sleep(2 * time.Second) // タスクの処理に時間がかかると仮定
        }
        doneCh <- true
    }()

    <-doneCh
}

上のコードでは、taskChのキャパシティが満たされると新たなタスクの送信がブロックされバックプレッシャとなる。

silvasilva

WaitGroupの使い方

  • Add・・・終了を待つゴルーチン数のカウンタを指定した数だけ増やす
  • Done・・・カウンタをデクリメントする。処理が終了したときに呼ばれる
  • Wait・・・カウンタが0になるまで、それを含むゴルーチンをポーズする
    以下具体例。
package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	wg.Add(3)

	go func() {
		defer wg.Done() // ゴルーチンがパニックになっても実行されるようdeferをつける
		fmt.Println("Goroutine 1")
	}()

	go func() {
		defer wg.Done()
		fmt.Println("Goroutine 2")
	}()

	go func() {
		defer wg.Done()
		fmt.Println("Goroutine 3")
	}()

	wg.Wait()
	fmt.Println("All Goroutines Finished")
}
silvasilva

sync.Onceの使い方

package main

import (
    "fmt"
    "sync"
)

var once sync.Once // sync.Onceのインスタンスの宣言は通常関数外で行う

func doOnce() {
    fmt.Println("一度だけ実行")
}

func main() {
    for i := 0; i < 5; i++ {
        once.Do(doOnce)
    }
}

// 出力
// 一度だけ実行
silvasilva

Jsonの取り扱い

Goのデータ型 -> Json (アンマーシャリング)

package main

import (
	"encoding/json"
	"fmt"
)

var f = struct {
    Name string // キー名は必ず大文字から!
    Age int 
}{}

func main() {
    // 第2引数には必ずfのポインタを渡す!
    err := json.Unmarshal([]byte(`{"Name":"John", "Age":30}`), &f) 

    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Printf("%+v\n", f)
}

Json -> Goのデータ型 (マーシャリング)

以下Jsonのサンプル。

{
    "users": [
      {
        "id": 1,
        "name": "山田 太郎",
        "email": "taro.yamada@example.com",
        "roles": ["admin", "user"]
      },
      {
        "id": 2,
        "name": "佐藤 花子",
        "email": "hanako.sato@example.com",
        "roles": ["user"]
      }
    ]
 }

以下、JsonGoのデータ型に変更するコード。

import (
	"encoding/json"
	"fmt"
	"io"
	"os"
)

// 型の次に`json:タグ名`を指定する
// 各フィールドのキーは大文字から!
type User struct {
    ID int `json:"id"`
    Name string `json:name`
    Email string `json:email`
    Roles []string `json:roles`
}

type Users struct {
    Users []User `json:users`
}

func main() {
    jsonFile, err := os.Open("test.json")
    if err != nil {
        fmt.Println(err)
    }

    fmt.Println("Jsonファイルを正常に開きました")
    defer jsonFile.Close()

    byteFile, _ := io.ReadAll(jsonFile)

    var users Users

    json.Unmarshal(byteFile, &users)

    for i := 0; i < len(users.Users); i++ {
        fmt.Println("User ID: ", users.Users[i].ID)
        fmt.Println("User Name: ", users.Users[i].Name)
        fmt.Println("User Email: ", users.Users[i].Email)
        fmt.Println("User Roles: ", users.Users[i].Roles)
    }
}

silvasilva

もっと効率よくJSONを読み書きする方法

以下のようにjson.Encode, json.Decodeを使う

import (
	"encoding/json"
	"fmt"
	"os"
)

type Person struct {
    Name string `json:"name"`
    Age int `json:"age"`
}

func main() {
    dataToFile := Person{
        Name: "フレッド",
        Age: 20,
    }

    tmpFile, err := os.CreateTemp(os.TempDir(), "sample-")
    defer os.Remove(tmpFile.Name())
    err = json.NewEncoder(tmpFile).Encode(dataToFile)

    if err != nil {
        panic(err)
    }

    err = tmpFile.Close()
    if err != nil {
        panic(err)
    }

    tmpFile2, err := os.Open(tmpFile.Name())
    if err != nil {
        panic(err)
    }

    var dataFromFile Person
    err = json.NewDecoder(tmpFile2).Decode(&dataFromFile)
    if err != nil {
        panic(err)
    }

    err = tmpFile2.Close()
    if err != nil {
        panic(err)
    }

    fmt.Println("読み込んだデータ")
    fmt.Printf("%+v\n", dataFromFile)
}
silvasilva

テストコード

テストファイル名

テストコードを書くにはファイル名を_test.goで終わるようにする。foo.goというファイルを作った場合はfoo_test.goとする。

テストコード

// adder.go
func addNumbers(x, y int) int {
    return x + y
}
// adder_test.go
func Test_addnumbers(t, *testing.T) {
    result := addNumbers(2, 3)
    if result != 5 {
          t.Error("結果が違う: 期待する結果 5, 得られた結果 result")
    }
}
silvasilva

テスト前の設定の適用と解除

テストを実行する前に特定の状態を設定して置き、テスト終了後にそれを解除する場合は以下のようにする。

package adder

import (
	"testing"
	"fmt"
	"time"
	"os"
)

var testTime time.Time

func TestMain(m *testing.M) {
	fmt.Println("テストの準備")
	testTime = time.Now()
	exitVal := m.Run()
	fmt.Println("テストの後片付け")
	os.Exit(exitVal)
}

func TestFirst(t *testing.T) {
	fmt.Println("TestFirstではMainTestで設定されたものを使う", testTime)
}

func TestSecond(t *testing.T) {
	fmt.Println("TestSecondでもMainTestで設定されたものを使う", testTime)
}