1
1

go言語 チートシート

Last updated at Posted at 2024-05-21

はじめに

私自身はrailsを3年ほど中心に扱ってきましたが、転職先でgoを使用するためキャッチアップのために今回学習し始めました。go言語を全く知らない状態から学習しましたので、それに自分のコメントをつけたメモです。定期的に新しい情報や説明を加える予定です。

今回は以下の3つのUdemy講座を視聴しました。どれも分かりやすい講座でした。一部goのバージョンによってはうまくいかない講座もあるのでその場合は適宜調整が必要です。(そこまで大きな調整ではありませんので、大丈夫だと思います。)

A Tour of Goも有名ですが、Udemy講座で十分だと思います。

導入

  • ドキュメント
    https://go.dev/doc/#references
    * コマンド godocでも調べられる。
  • gofmt -w
    フォーマットを揃える
  • go build file.go
  • go build -o example file.go // exampleというファイル名でbuildできる
  • go run file.go
    build + 実行

ライブラリのインポート

  • 複数インポートする場合は()で囲む
  • 階層の場合は/で区切る

外部ライブラリのインポート
外部ライブラリをインポートする場合は
go mod init nameでmodファイルを作成し、go mod tidyで自動でインポートしてくれる

go modによる初期化

パッケージというのは「同じディレクトリの中にまとめられた、変数や定数、関数定義の集合」で
す。

/some_directory
|-- dir1 # サーバーを動かすためのパッケージ
|-- dir2 # サーバーからデータベースに接続しデータを取得するパッケージ
|-- 以下略

そしてモジュールというのは「パッケージの集まり」のことをいいます。

作成したディレクトリをGoのモジュールとしてつ開けるようにするためにはgo modコマンドを使用します。

go mod init github.com/yourname/reponame

すると、go.modというファイルが自動生成されていると思います。この go.mod というファイルがある場所が、モジュールのルートディレクトリとしてみなされるようになります。

go.mod
module directory_name

go 1.21.6

変数宣言

var

  • 変数宣言
  • 代表的な型
    string, int, bool, float32, float64, int8, int16, int32, int64
func main() {
    // 整数型
    var i int = 1

    // バイト型
	// int8, int16, int32, int64
	var num1 int32 = 1

    // 浮動小数点型
	// float32, float64

    // boolean型
    var t bool = true
    var f bool = false
    // 下のようにまとめることもできる
    var t, f bool = true, false
 
	var num1_1 float32 = 1.1
	num2 := 1.2

さらに、varをimportの時と同様に、()で囲んでまとめても書ける

var (
	i int = 1
	f64 float64 = 1.234
	st string = "hello"
	t, f bool = true, false
)

さらに、:=を使うことで、型推論で簡潔に書ける

xi := 1
xf64 :=1.12
xs := "test"
xt, xf := true, false

varと型推論の使い分け

  • 関数外ではvarしか使えないのでvarを使用する。
  • 明示的に型を指定したい場合はvarを使用する。
  • 次に説明するゼロ値になる場合はあえてvarを使用する。
    例えば、 num := 0とせず、var num intで良い。
  • :=は明示的な代入が必要、varは不要

ゼロ値

何も代入しない初期化された状態だと以下の通りとなる。

ゼロ値
string ""
int 0
int8 0
int16 0
int32 0
int64 0
uint 0
uint8 0
uint16 0
uint32 0
uint64 0
uintptr 0
float32 0
float64 0
complex64 (0+0i)
complex128 (0+0i)
bool false
array 各要素のゼロ値
slice nil
map nil
chan nil
func nil
interface nil
pointer nil
struct 各フィールドのゼロ値

const

  • 上書きできない定数に使用
package main

const Pi = 3.14

const (
	Usernmae = "test_user"
	Password = "test_pass"
)

func main() {
Pi = 3 // エラーが起きて実行失敗
}

型と型変換

文字列型

var s string = "Hello World"
var s_2 string = "Hello" + "World"

// 文字列の配列を指定するとASCIIコードが表示される
"Hello"[0] 
// => 72
// このコードを文字列で出したいときはstringsをimportして使用
strings("Hello"[0])

// 文字列の指定した文字を変えたい場合はstringをimportして、strings.Replaceで使用
s = "Hello World"
strings.Replace(s, "H", "X", 1)

// stringsで他にはContainsで指定文字列が含まれているかどうかチェックできる
s = "Hello World"
strings.Contains(s, "World")

バイト型

b := []byte{72, 73}
string(b) // "HI"

c := []byte("HI") // 72, 73
string(c) // "HI"

*その他の数値型や論理値型はrubyと大差ないので割愛。

型変換

  • 数値同士の変換やASCIIの変換はできるが、文字列から数値への変換はstrconvを使用する必要がある

数値の型変換は以下のように、容易にできる。

var x int = 1
xx := float64(x) // xxの型はintからfloat64へ変換

var y float64 = 1.23 
yy := int(y)  // yyの型はfloat64からintへ変換

しかし、文字列を数値にするには他の言語と異なって、以下のようにAtoi(Ascii to integer)をimportして型変換する必要がある。

Atoiはintもしくはerrを返却し、変換できる場合はint、できない場合はerrを返す。

var s string = "12"
i, _ := strconv.Atoi(s) // 12

var s string = "hoge"
i, _ := strconv.Atoi(s) // err 

generics

  • 型をカスタマイズして使用することで柔軟性を持たせられる

以下のようにいくつもの型を組み合わせて使用する。

package main

import (
	"fmt"
)
func main(){
	fmt.Printf("1+2:%v\n", add(1,2)) // 1+2:3
	fmt.Printf("あ+い:%v\n", add("あ","い")) // あ+い:あい

}
type customConstraints interface {
	int | int16 | float32 | float64 | string
}
func add[T customConstraints](x, y T) T {
	return x + y
}

チルダ

  • 基本の型に独自の型を含められる

例えば、NewIntという型をintで宣言して、~をcustomConstraintsの中のintにつける。
すると、intに紐づいた独自の型も含むこととなる。簡単にいうとintの仲間に入れてくれるってこと。

type customConstraints interface {
    `~`をつけることでintを宣言した独自の型も仲間となる
	~int | int16 | float32 | float64 | string
}

type NewInt int // int型で宣言

func main(){
    var i1, i2 NewInt = 3, 4
    add(i1, i2)
}

func add[T customConstraints](x, y T) T {
	return x + y
}

constraintsパッケージ

あらかじめgoで型が用意されている。
https://pkg.go.dev/golang.org/x/exp/constraints

例えば、constrains.Orderedを使ってみる。ちなみにOrderedの定義は以下の通りでInteger | FLoat | ~stringとなっている。

type Ordered
type Ordered interface {
	Integer | Float | ~string
}
func main(){
	m1 := map[string]uint{
		"A": 1,
		"B": 2,
		"C": 3,
	}

	m2 := map[int]float32{
		1: 1.23,
		2: 4.56,
		3: 7.89,
	}

	result1 := sumValues(m1)
	result2 := sumValues(m2)
    result3 := minValue(1, 10)

	fmt.Printf("%v\n", result1) // 6
	fmt.Printf("%v\n", result2) // 13.68
	fmt.Printf("%v\n", result3) // 1
}

// 引数や返り値を組み合わせたgenerics K, Vを定義し、引数と返り値の型として使用
func sumValues[K int | string, V constraints.Float | constraints.Integer](m map[K]V) V {
	var sum V
	for _, v := range m {
		sum += v
	}

	return sum
}

// constraints.Orderedのgenerics Tを定義し、引数と返り値の型として使用
func min[T constraints.Ordered] (x, y T) T {
	if x < y {
		return x
	}
	return y
}

配列

配列とスライス

  • array 配列は数が決められている
  • 個数を決めたくない場合はスライスを使用する
// 2個整数を保持する配列を定義
var a [2]int
a[0] = 100
a[1] = 200
// 同じ結果
var b [2]int = {100, 200}

// ただし、配列は個数を変更できないため以下のように書くとエラーとなる
b = append(b, 300)

一方で、個数を指定する必要がない場合はスライスを使用する。
配列とは異なり、数の指定がないためエラーは起きない。

// 以下のように[]に数値を設定しなければエラーは起きない
var b []int = []int{100, 200}
c = append(b, 300) // [100, 200, 300]

// もちろん型推論で書くことも可能
var n []int = []int{1, 2, 3, 4, 5, 6}
var n := []int{1, 2, 3, 4, 5, 6}

// 取得は他の言語と同じ
x = n[2] // 3を取得できる

また、sliceにsliceを追加する場合は以下のように...で式展開してappendする。

s1 := []int{1, 2, 3}
s2 := []int{4, 5, 6}
result := append(s1, s2...)

インデックスの範囲を指定する書き方もできる。
若干ややこしいが、開始インデックスは含まれる、終了インデックスは含まれない。

つまり、n[start:end]の場合、startからend-1までの要素を含む新しいスライスを生成することとなる。

// インデックス start から end-1 までの要素を含む新しいスライスを生成する
n[2:4]
// n[0:end] と同じで、最初の要素から end-1 までの要素を含む
n[:2]
// n[start:]:インデックス start から配列の最後までの要素を含む
n[2:]  //[3 4 5 6]
// n[:]:配列の全要素を含む新しいスライスを生成する
n[:]  //[1 2 3 4 5 6]

また、多次元配列は以下のように書ける。

b := [2][3]int{{1, 2, 3}, {4, 5, 6}}
fmt.Println(b[0][0])
fmt.Println(b[0][1])
fmt.Println(b[0][2])	
fmt.Println(b[1][0])	
fmt.Println(b[1][1])
fmt.Println(b[1][2])

スライスの場合もほぼ同じ。

// [[0 1 2] [3 4 5] [6 7 8]]
var board := [][]int{
	[]int{0, 1, 2},
	[]int{3, 4, 5},
    []int{6, 7, 8},
}

他の違いとしては、以下のように書いた場合varはnilだが、:=の場合はnilではない。

var s1 []int
s2 := []int{}

makeを使ってスライスを定義

makeを使用すると指定したキャパシティの値に応じてメモリ領域を確保してくれる。

s := make([]int, 0, 2) // 要素数が0でキャパシティーが2のスライスを生成

ただ、注意が必要なのはスライスを切り取って使用する場合はメモリを共有するので初期化しても値が変わってしまう。

func main(){
	s1 := make([]int, 4, 6)
	fmt.Println(s1) // [0 0 0 0]
	s2 := s1[1:3] // ここでs1のindex1と2を切り取ってs2に代入
	fmt.Println(s2) // [0 0]
	s2[1] = 10
	fmt.Println(s1) // ここで通常であれば影響は受けなさそうだが、[0 0 10 0]となる
	fmt.Println(s2) // ここは当然、[0 10]となる
}

メモリを共有したくない場合はcopy関数を使用する
部分的に共有したい場合は、以下のように切り取る際の最後にインデックスを指定する。インデックス-1まで共有することとなるため、0〜2までが共有されるが、それ以上は共有されない。

s1 = make([]int, 4, 6)
s2 := s5[1:3:3]

map

  • rubyのハッシュと同じだね
func main() {
    m := map[string]int{"apple": 100, "banana": 200}
    m["apple"] // 100
    m["banana"] = 300 // 300を200に代入
    m["orange"] = 400 // orange: 400が追加される
}

返却値は値と存在するかどうかのboolean判定

m := map[string]int{"apple": 100, "banana": 200}
v, exists = m["apple"] // vは100、existsはtrue
v, exists = m["nothing"] // vは0、existsはfalse

削除する場合は、delete関数を使用して、マップとkeyを指定すれば良い。

delete(m, "Key")

もう少し踏み込んでStructと使ってみると以下のように構造体を作成しtaskMapを作って、タスクを追加してみる。

type Task struct {
    ID          string
    Title       string
    Description string
}

taskMap := make(map[string]*Task)

// タスクをいくつか追加
taskMap["task1"] = &Task{ID: "task1", Title: "タスク1", Description: "タスク1の説明"}
taskMap["task2"] = &Task{ID: "task2", Title: "タスク2", Description: "タスク2の説明"}

するとtaskMapは以下のような形となる。

map[string]*Task{
    "task1": &Task{ID: "task1", Title: "タスク1", Description: "タスク1の説明"},
    "task2": &Task{ID: "task2", Title: "タスク2", Description: "タスク2の説明"}
}

したがって、キーはタスクのID("task1", "task2"など)で、値は Task 構造体のポインタとなります。このポインタは、タスクのID、タイトル、説明などの情報を含む Task 構造体を指しています。

だから、このキーを使用して以下のように構造体にアクセスすることが可能となる。

task1 := taskMap["task1"] // "task1" の Task 構造体のポインタを取得
fmt.Println("タスクID:", task1.ID)       // タスクのIDを表示
fmt.Println("タイトル:", task1.Title)   // タスクのタイトルを表示
fmt.Println("説明:", task1.Description) // タスクの説明を表示

関数

  • 他の言語と変わらないかな
  • 引数と返り値の型を定義する

定義した関数をmain関数から呼び出して実行。

// 引数をたす関数
// 型が同じ場合は(x, y int)とも書ける
func add(x int, y int) int {   
    return x + y
}

func main(){
    add(1, 2) // 3
}

さらに、複数の返り値を返すことも可能

func add(x, y int) (int, int) {   
    return x + y, x - y
}

func main(){
    r1, r2 := add(1, 2)
    r1 // 3
    r2 // -1
}

また、返り値の変数名を書くことでreturnだけで済む

func calc(x, y int) (result int) {   
    result = x * y // :=にすると初期宣言ではないためエラーとなる
    return // ここでreturn resultとする必要はない
}

func main(){
    r := calc(1, 2)
    r // 2
}

関数を変数に入れて使うこともできるし、そのまま実行することもできる。

func main(){
    f := func(x int){
        fmt.Println("func!", x)
    }

    f(1)
}

// または変数に格納せずにそのまま実行
func main(){
    func(x int){
        fmt.Println("func!", x)
    }(1)
}

さらに、関数の引数に関数を渡すことももちろん可能。
以下の例はまあこんな書き方しなくてもいいけど例ということでご愛嬌

func main(){
	f := func(x int) int{
		return x * 2
}

result := someFunc(f, 2)
fmt.Printf("result: %v", result)
}

func someFunc(f func(x int) int, num int) int{
    return f(num)
}

さらには関数の中で無名関数を返却することも可能。

func main(){
	f := multiply()
	result := f(1)
	fmt.Printf("result:%v", result) // 1000
}

// この場合、引数は無しで、返り値の型のみ定義
func multiply() func(int) int {
	return func(n int) int{
			return n * 1000
	}
}

可変長引数

...typeを使用すると可変長引数にできる

func foo(params ...int){
}

反対にslice...のように使うと式展開できる

func foo(params ...int){
    ...
}

s := []int{1, 2, 3}
foo(s...)

if

  • まあこれも他の言語と変わらないな
  • &&も||の論理和も同じなので割愛
func main() {
    var num int = 4 // num := 4
    if num % 2 == 0 {
        fmt.Println("2で割れます!")
    } else if num % 3 == 0 {
        fmt.Println("3で割れます!")
    } else {
        fmt.Println("2でも3でも割れません!")
    }
}

少し応用的な書き方をしてみると(条件分岐は例のために書いているの意味なし笑)

import "fmt"

func double(x, y int) string{
    if x * y == 2 {
        return "OK"
    } else {
        return "NG"
    }
}

func main(){
    result := double(1, 2)
    if result == "OK" {
        fmt.Println("OK")
    }

    // この条件分岐を1行で書いてみると以下の通りとなる。
    // 「もしresultに結果を入れて、そのresultがOKなら」って感じかな
    // ただし、この場合はのちにresultを使おうとしても使えず、この実行で完結するので注意!
    if result := double (1, 2); result == "OK"{
        fmt.Println("OK")
    }
}

for

func main(){
    for i := 0; i < 10; i++ {
        fmt.Println(i) // 0~9まで出力される
    }
}

また、rubyのnextのようにcontinueで処理を次に移すことも可能

// この場合は3の場合はifブロック内に入り、その後は処理されない。
func main(){
    for i := 0; i < 10; i++ {
        if i == 3 {
            fmt.Println("3だよ!")
            continue
        }
        fmt.Println(i)
    }
}

当然、continueがあるならbreakもあります。

// この場合は、5になったらbreakでループが終了する
func main(){
    for i := 0; i < 10; i++ {
        if i == 6 {
            fmt.Println("終わりだよ!")
            break
        }
        fmt.Println(i)
    }
}

for range

  • 配列やマップから値を取り出すのに便利!
  • 配列はindexとvalue
  • マップはkeyとvalue

例えば、配列を表示するときに普通に思いつくのはこんな感じ

func main() {
    l := []string{"java", "go", "ruby"}
    for i := 0; i < len(l); i++{
        fmt.Println(i, l[i])
    }
}

でも、for rangeを使えばもっと便利!!

  • 配列の場合
func main() {
    l := []string{"java", "go", "ruby"}
    for i, v := range l{
        fmt.Println(i, v)   
    }

    for _, v := range l{
        fmt.Println(v)
    }
}
  • マップの場合
func main() {
    m := map[string]int{"apple": 100, "banana": 200}
    for k, v := range m{
        fmt.Println(k, v)  
    }

    for _, v := range m{
        fmt.Println(v)  
    }
}

for rangeの注意点

スライスをfor rangeで取り出す場合コピーが使用されるので元の値は更新されないことに注意

package main

import (
    "fmt"
)

// item 構造体の定義
type item struct {
    price float32
}
func main() {
    // スライスの初期化
    items := []item{
        {price: 10.0},
        {price: 20.0},
        {price: 30.0},
    }

    // 価格を1.1倍にする
    for i := range items {
        i.price *= 1.1 // これだと構造体のコピーが生成されているためitemsは更新されない
        items[i].price *= 1.1 // これは更新される
    }

    // items[i].priceのスライスの内容を表示
    fmt.Printf("%+v\n", items) // [{price:11} {price:22} {price:33}]
}

defer

  • 遅延実行で関数が終了したタイミングで実行する
  • FIFOではなく、LIFOなので注意!

例えば、以下のように書いた場合、通常は上から順次実行されるが、deferを使用しているので
helloの後にwolrdが表示される

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

ログ

  • 一般的なdebugやwarnなどは標準では用意されていない
  • log.Println,log.Printg,log.Fatalln、またはサードパーティを利用する
func main(){
    log.Println("logging!")
    log.Printf("%T %v", "test", "test")

    log.Fatalf("%T %v", "test", "test") // この時点でexitする
    log.Fatalln("error!") // この時点でexitする
}

使い方としては、こんな感じで書ける。

func main(){
    _, err = os.Open("aaaa")
    if err != nil{
        log.Fatalln("err", err)
    }
}

エラー

Go ではエラーはインターフェースとしてされています。

type error interface {
Error() string
}

つまり、「Error メソッドがある構造体は等しくエラーとして扱われる」ということです。

errors.Newを使用する

これは最も基本的なエラーの作成方法です。

import (
    "errors"
    "fmt"
)

func main() {
    err := errors.New("an error occurred")
    fmt.Println(err)
}

fmt.Errorfを使用する

fmt.Errorfを使用すると、フォーマットされたエラーメッセージを作成できます。

import "fmt"

err := fmt.Errorf("an error occurred: %s", "something went wrong")

カスタムエラー型を定義する

エラーの詳細を含むカスタムエラー型を定義することができます。

String()を型に実装すれば使えるのと同様にError()も型に紐づければカスタマイズしたエラーを出力できます。

type MyError struct {
    Code    int
    Message string
}

func (e *MyError) Error() string {
    return fmt.Sprintf("Error %d: %s", e.Code, e.Message)
}

err := &MyError{Code: 404, Message: "resource not found"}

カスタムエラーを作成する手順

1.エラー構造体の定義

package main

import (
	"fmt"
)

// CustomErrorは独自のエラー構造体です。
type CustomError struct {
	Message string
	Code    int
}

2.Errorメソッドの実装

// Errorメソッドはエラーインターフェースを実装します。
func (e *CustomError) Error() string {
	return fmt.Sprintf("Error %d: %s", e.Code, e.Message)
}

3.独自のエラーを作成して使用
構造体を初期化してエラーとして返し、エラーハンドリングを行います。

// NewCustomErrorは新しいCustomErrorを作成します。
func NewCustomError(message string, code int) error {
	return &CustomError{
		Message: message,
		Code:    code,
	}
}

// main関数は例として独自のエラーを作成して使用します。
func main() {
	err := NewCustomError("Something went wrong", 123)
	if err != nil {
		fmt.Println(err)
	}

	// 型アサーションを使用してカスタムエラーの詳細にアクセス
	if customErr, ok := err.(*CustomError); ok {
		fmt.Printf("Custom error details - Code: %d, Message: %s\n", customErr.Code, customErr.Message)
	}
}

errorsパッケージのAsおよびIs関数を使用する

Go 1.13以降では、errorsパッケージにAsおよびIs関数が追加され、エラーハンドリングがより柔軟になりました。第一引数のエラーが第二引数のターゲットと等しいかどうかをbooleanで判定できます。

import "errors"

var ErrNotFound = errors.New("resource not found")

err := doSomething()
if errors.Is(err, ErrNotFound) {
    fmt.Println("The error is ErrNotFound")
}

引用:
errors.Is 関数というのは、エラーがラップされていた場合でも正しくエラーの比較ができるように設計されています。今まで errors.Is 関数は「引数に与えられてたエラー2つが等しいかどうかを判定するための関数」と説明してきましたが、実は正確には「第一引数で与えられたerrからのエラーチェーンの中に、第二引数で与えられたエラーtargetが含まれているかを判定する関数」というのが正しいです。

第一引数で与えられたエラー err が、第二引数で与えられた target に変換可能であれば、
結果を target 変数に格納した上で戻り値 true を返す
• 第一引数で与えられたエラー err が、第二引数で与えられた target に変換不可能であれば、
戻り値 false を返す

errorsパッケージのUnwrap関数を使用する

errors.Unwrap()は、カスタムエラー構造体のerror型のフィールド(例:Errフィールド)の値を返します。具体的には、渡された引数である構造体がUnwrapメソッドを実装している場合、そのUnwrapメソッドを実行して元のエラーを返します。

package main

import (
	"errors"
	"fmt"
	"strconv"
)

// CustomErrorは独自のエラー構造体です。
type CustomError struct {
	Message string
	Err     error
}

// Errorメソッドはエラーインターフェースを実装します。
func (e *CustomError) Error() string {
	return fmt.Sprintf("%s: %v", e.Message, e.Err)
}

// Unwrapメソッドは元のエラーを返します。
func (e *CustomError) Unwrap() error {
	return e.Err
}
func main() {
	// strconv.Atoiでエラーを発生させる
	_, err := strconv.Atoi("invalid")

	// 発生したエラーをCustomErrorでラップする
	customErr := &CustomError{
		Message: "変換エラー",
		Err:     err,
	}

	// CustomErrorの型と内容を出力
	fmt.Printf("customErr: [%T] %v\n", customErr, customErr)

	// Unwrapを使って元のエラーを取り出す
	originalErr := errors.Unwrap(customErr)
	fmt.Printf("originalErr: [%T] %v\n", originalErr, originalErr)
}

エラーをラップ、アンラップする

MyAppErr構造体を作成し、Error関数を実装することでerrorインターフェースを満たす

type MyAppError struct {
	ErrCode
	Message string
	Err error `json:"-"`
}
type error interface {
	Error() string
}

そして、Err errorフィールドを用意し、そこにアンラップするコードをかく。

func (myErr *MyAppErr) unwrap() error {
    return myErr.Err
}

次にラップするコードをerrcode.goに記述する。

package apperrors

type ErrCode string

const (
	Unknown 					ErrCode = "U000"
	InsertDataFailed 	ErrCode = "S001"
	GetDataFailed 		ErrCode = "S002"
	NAData 						ErrCode = "S003"
)

func (code ErrCode) Wrap(err error, message string) error {
	return &MyAppError{ErrCode: code, Message: message, Err: err}
}

こうすることで、MyAppError構造体のErrフィールドに発生したエラーをラップすることができ、またその逆も同じで、構造体のエラーフィールドからErrフィールドを取得することもできる。

main.go
	if err := json.NewDecoder(req.Body).Decode(&reqComment); err != nil {
		err = apperrors.RedBodyDecodeFailed.Wrap(err, "bad request body")
		apperrors.ErrorHandler(w, req, err)
		return
	}

ポインタ

  • メモリアドレスを使用して、値を格納、取得する
  • &はメモリアドレスを指す
  • メモリアドレスに"*"をつけると値を指す
  • makeはポインタを返さず、newはポインタを返す
func main() {
    var n int = 100

    fmt.Println(&n) // 0xc00007c008

    var p *int = &n

    fmt.Println(p)  // 0xc00007c008

    fmt.Println(*p)
}

ただ、これだと var n int = 100のように設定してからvar n *int = &nでポインタを設定する必要があるし、初期値を設定したくない場合も当然ながらある。

そのような時にnewを使うことでポインタのメモリアドレスを確保できる

    var p *int = new(int)
    fmt.Println(p) // 0xc00000~ とメモリアドレス返ってくる
    fmt.Println(*p) // 初期値0が返ってくる

    var p2 *int
    fmt.Println(p2) // p2は型を宣言しているだけなのでnilが返ってくる

もう少し深掘りすると、ポインタはメモリ内の特定の1byteの番地
&をつけるとメモリに配置された先頭のメモリアドレスを取得できる。

例えば、以下のコードを実行してみると出力結果は、次のとおり

var value uint16
fmt.Printf("memory address: %p\n", &value)
var value2 uint16
fmt.Printf("memory address: %p\n", &value2)

// memory address: 0xc000120066
// memory address: 0xc000120068

この例から、uint16型の変数が2バイト分のメモリを確保するため、value と value2 のメモリアドレスの差が 2 になっていることがわかります。

ポインタ変数は特定のメモリアドレスを指し示しますが、そのメモリアドレスのどこまでがその変数に関連するかは、そのポインタの型情報に依存します。たとえば、uint16 型のポインタ *uint16 は、そのアドレスから 2 バイト分がその変数に関連すると解釈されます。

したがって、ポインタ変数には必ず型情報を与える必要があります。これにより、ポインタが指し示すデータのサイズを正しく解釈できます。

さらに、ポインタ変数を使って以下のように&で取得したメモリアドレスを*typeで格納する。

var value uint16
fmt.Printf("memory address: %p\n", &value)
var value2 uint16
fmt.Printf("memory address: %p\n", &value2)

// valueの先頭メモリアドレスを格納
*でポインタ型にすることでメモリアドレスを格納する用に使用することを意味する
var p1 *uint16 = &value
fmt.Printf("memory address: %p\n", p1)
fmt.Printf("memory address: %p\n", &p1)
fmt.Printf("memory address: %v\n", *p1)

なので、&p1とvalueは当然異なるメモリアドレスとなる。

memory address: 0xc00009e066 //value
memory address: 0xc00009e066 //p1
memory address: 0xc000090050 //&p1

で、ここでp1自体はvalueのメモリアドレスとは異なるが、p1は0xc00009e066というvalueのメモリアドレスを格納している。

そこで、defrenceと言って*pointerのように*を使うことで格納しているメモリアドレスの値を取得することができる。

例えば、この場合*p1とすると、p1にはvalueのメモリアドレスが入っているので、そのメモリアドレスの値(初期値なので0)を取得できるということ。

まあざっくり言えばポインタ型はメモリアドレスを格納しておく場所のようなもので、それを使えば値も取れるってこと。

スクリーンショット 2024-05-18 14.57.34.png

ダブルポインタ

上記が理解できればダブルポインタもそんなに難しいことではない。
箱であるポインタに入れたメモリアドレスを、別の箱であるポインタに入れるってこと。マトリョーシカ的な感じだね。

*だと1回のdereferenceを行う。
つまり、ポインタ変数が保持しているメモリアドレスを元に、そのアドレスに格納されている値にアクセスできる

スクリーンショット 2024-05-18 22.56.13.png

Structオリエンテッド

  • rubyのインスタンス変数の型付きみたいなイメージ
type Vertex struct{
    X, Y int
}

func Area(v Vertex) int{
    return v.X * v.Y
}

func main(){
    v := Vertex{3, 4}
    Area(v) // 12
}

上のコードでVertexにArea関数を結びつけて使える

type Vertex struct{
    X, Y int
}

// VertexにArea関数のattribute_accessorを付与するイメージ
func (v Vertex) Area() int{
    return v.X * v.Y
}

func main(){
    v := Vertex{3, 4}
    fmt.Println(v.Area())
}

以下のように大文字で書くとパッケージ外から呼び出せるが、

type Person struct{
    Name string
    Age int
}

小文字で書くとパッケージ外から呼び出せないので注意!

type person struct{
    name string
    age int
}

これは変数と関数に関しても同じ。

また、以下のようにtaskを作成して、別の変数に格納してその変数の値(今回はTitle)を変更しても元のtaskは影響を受けない。

func main(){
task := Task{
	Title: "hoge",
	Content: "hoge",
}

var task2 Task = task
task2.Title = "hoge2"
fmt.Printf("task: %v task2: %v\n", task.Title, task2.Title)
}

type Task struct{
	Title string
	Content string
}

元の値を変更したい場合はdereferenceを使用して、メモリアドレスのバリューを取得し更新する。

func main(){
task := &Task{
	Title: "hoge",
	Content: "hoge",
}

(*task).Title = "hogehoge" // 構造体のフィールドに対してdereferenceする場合は*を省略可能
fmt.Printf("task: %v\n", task.Title)
}

Embedded

  • 既に定義したstructを使用して、structを定義できる
// Person structは個人の基本情報を表す
type Person struct {
    Name string
    Age  int
}

// Employee structはPerson情報を埋め込み、その人が所属する部署を表す
type Employee struct {
    Person      // PersonalInfo Personのように書くことも可能
    Department string
}

// フィールド名は省略もできる
func main() {
    emp := Employee{
        Person: Person{
            Name: "John Doe",
            Age:  30,
        },
        Department: "Engineering",
    }

    // 埋め込みによりPersonのフィールドへ直接アクセス
    fmt.Println("Employee Name:", emp.Name) // emp.Person.Name と同じ
    fmt.Println("Employee Age:", emp.Age)   // emp.Person.Age と同じ
    fmt.Println("Employee Department:", emp.Department)
}

インターフェース

  • 要求するメソッドを定義することで、どのようなメソッドを持つべきか振る舞いを記述できる
// Interface インターフェース
type Interface interface {
    Say() string
}

// Person 構造体
type Person struct {
    Name string
}

// Dog 構造体
type Dog struct {
    Name string
}

// PersonにSayメソッドを紐づける
func (p *Person) Say() string {
    p.Name = "Mr." + p.Name
    return p.Name
}

// DriveCar 関数
func DriveCar(human Interface) {
    if human.Say() == "Mr.Mike" {
        fmt.Println("Run")
    } else {
        fmt.Println("Get Out!")
    }
}

func main() {
    // Person構造体はSay関数が紐づいているのでinterfaceで問題なし
    var mike Interface = &Person{Name: "Mike"}

    DriveCar(mike)

    var dog Interface = &Dog{Name: "Pochi"}
    DriveCar(dog) // Dog構造体はSay関数を持っていないためコンパイルエラーになる
}

タイプアサーションとswitch type文

  • goでは引数に複数の型を指定できない
  • その場合にinterface{}を使用する
  • キャストとは若干意味合いが違うので一応押さえておく
func do(i interface{}){
    ii := i.(int) // タイプアサーション(インターフェースの型を変える)
    ii *= 2
    fmt.Println(ii)

    ss := i.(string)
    fmt.Println(ss + "!")
}

func main(){
    do(10) // 20
    do("Mike") // Mike!
    do(true)
}

上をswitch type文で書き換えるとスッキリする

func do(i interface{}){
    switch v := i.(type){
    case int:
        fmt.Println(v * 2)
    case string:
        fmt.Println(v + "!")
    default:
        fmt.Println(v)
    }
}

func main(){
    do(10) // 20
    do("Mike") // Mike!
    do(true) // true
}

Goroutine

  • 並行処理を簡単に実装できる

  • A->B の順番で実装されているけれど、実は B が A を待たなくていい場所

  • 同時に実行しても問題ない場所

func testroutine(s string, wg *sync.WaitGroup){
    for i := 0; i < 5; i++{
        fmt.Println(s)
    }
    wg.Done()
}

func normal(s string){
    for i:=0; i < 5; i++{
        fmt.Println(s)
    }
}

func main(){
    var wg sync.WaitGroup
    wg.Add(1) // 並行処理が1つあることを設定
    go goroutine("World", &wg)
    normal("hello")
    wg.Wait() // wg.Done()が完了するまで待機する
}

新しい goroutine を作成すると、その goroutine が割り当てられたタスクを実行しますが、そのタスクが実行されている間、メインの goroutine 別名 main 関数がそのタスクを実行し続け、メインのゴルーチンはタスクを完了し、プログラムは終了します。

つまり、main関数が終わると別のgoroutineは合流せずに終了してしまうこととなる。

xk2a4gv9astn8t5p6l1v.png

https://dev.to/angeldhakal/concurrency-in-go-using-goroutines-b1i より引用

そうならないためにsync.Waitgroupを使用することでmain関数はgoroutineが完了するのを待機することができる。

waitGropuの中ではカウンターが設定されていて、Addすることでカウントが+1される。
そして、Doneするとカウントが-1される。カウントが0になるまでWaitで待機を行う。

package main

import (
	"runtime"
	"fmt"
	"sync"

	"golang.org/x/exp/constraints"
)
func main(){
	var wg sync.WaitGroup
	wg.Add(1)
	go func(){
		defer wg.Done()
		fmt.Println("goroutine invoked")
	}()
	wg.Wait()
	fmt.Printf("num of working goroutines: %d\n", runtime.NumGoroutine())
	fmt.Println("main function done")
}

// 実行結果
// goroutine invoked
// num of working goroutines: 1
// main function done

channel

  • goroutineではreturnできないため状態を取得するために使用
func goroutine(s []int, c chan int){
    sum := 0
    for _, v := range s{
        sum += v
    }
    c <- sum // sumをcに入れる
}

func main (){
    s := []int{1, 2, 3, 4, 5}
    c := make(chan int)
    go goroutine(s, c)
    x := <-c // cに値が入ったらxに格納し、次の行へ進む
    fmt.Println(x) // 15
}

他の例も載せておく

package main

import (
	"fmt"
	"sync"
	"time"
)
func main(){
	ch := make(chan int )
	var wg sync.WaitGroup
	wg.Add(1)
	go func(){
		defer wg.Done()
		ch <- 10
		time.Sleep(500 * time.Millisecond)
	}()
	
	fmt.Println(<-ch) // 10
	wg.Wait()
}

また、別関数でチャネルを共有することも可能

func goroutine1(s []int, c chan int){
    sum := 0
    for _, v := range s{
        sum += v
    }
    c <- sum // sumをcに入れる
}

func goroutine2(s []int, c chan int){
    sum := 0
    for _, v := range s{
        sum += v
    }
    c <- sum // sumをcに入れる
}

func main (){
    s := []int{1, 2, 3, 4, 5}
    c := make(chan int) // チャネルがキューのようなイメージ 15, 15
    go goroutine1(s, c)
    go goroutine2(s, c)
    x := <-c // cに値が入ったらxに格納し、次の行へ進む
    fmt.Println(x) // 15
    y := <-c // cに値が入ったらyに格納し、次の行へ進む
    fmt.Println(y) // 15
}

チャネルを作成するときに、makeを使用する方法に注意してください。 「var ch chan struct{}」と言うのではなく。 varを使用すると、変数は型の「ゼロ」値で初期化されます。したがって、stringの場合は""、intの場合は0になります。

チャネルの場合、ゼロ値はnilであり、<-で送信しようとすると、nilチャネルに送信できないため、永久にブロックされます。

Buffered Channels

  • アンバッファードチャネルは、常に送信と受信がほぼ同時に行われるように強制するため、同期処理に適している
  • バッファリングされたチャネルは、送信と受信の間にタイムラグが許容される場合や、非ブロッキングの送信が必要な場合に便利
  • messages := make(chan string) と記述した場合、これはバッファサイズを指定しないアンバッファードチャネルを作成することを意味し、アンバッファードチャネルでは、送信された値は受信側の準備ができるまでチャネル内でブロックされる。具体的には、送信側(chan <-)は受信側(<- chan)がデータを受け取るまでデータを送信できず、処理が停止する点に注意が必要

例えば以下のコードではブロッキングが起きてしまう。

バッファ無しの場合はチャネルの受信が開始していないと、チャネルへの書き込みはできない。
このコードでは、チャネルへの書き込みが受信よりも先に行われるため、最初の行でプログラムはブロックされてしまう。

ch := make(chan int)
ch <- 10
fmt.Println(<-ch)

したがって、以下のようにgoroutineの中で送信を行い、受信される体制が整った場合にmainで受信される。

func main() {
    ch := make(chan int)

    // 別ゴルーチンを使わずに送信と受信
    go func() {
        ch <- 10  // ゴルーチン内で送信
    }()

    fmt.Println(<-ch)  // メインゴルーチンで受信
}

これを避けるためにバッファを指定することで、チャネルの受信の開始を待たずして指定したバッファの数だけ受信することができる。

例えば、バッファの数を2とした場合、3つ目を入れようとしているため、エラーが起きる。

func main(){
    ch := make(chan int, 2)
    ch <- 100
    fmt.Println(len(ch)) // 1
    ch <- 200
    fmt.Println(len(ch)) // 2
    ch <- 300
    fmt.Println(len(ch)) // Error
}

仮にループでチャネルから値を取り出そうとした場合にエラーが起きないためには、チャネルをcloseする必要がある。

func main(){
    ch := make(chan int, 2)
    ch <- 100
    fmt.Println(len(ch)) // 1
    ch <- 200
    fmt.Println(len(ch)) // 2

    // ここでクローズをしないと延々とチャネルから値を待ち続ける
    close(ch)
    for c := range ch{
        fmt.Println(c)
    }
}

また、channelを使用する際には注意点がある。

closeをしないとエラーが発生する主な理由
rangeループを使ったチャネルからの読み出しに関連しています。rangeを使ってチャネルから値を読み取る際、チャネルがcloseされていないと、ループがチャネルから次の値を待ち続け、それ以上送信される値がない場合にはゴルーチンがデッドロック状態に陥る可能性があります。つまり、closeされていないチャネルから値を読み取り続けると、送信元がもう値を送らないにもかかわらず受信側が値を待ち続ける状況が生じるためです。

チャネルのクローズとrangeループ
rangeループは、チャネルから値を読み出す一般的な方法ですが、以下のような挙動があります:

チャネルが開いている間:rangeはチャネルから継続的に値を読み出します。
チャネルが閉じられたとき:すべての値が読み出されると、rangeループは自動的に終了します。
チャネルをcloseしない場合、rangeループは新しい値がチャネルに送られるのを無限に待ち続けます。もし送信側がこれ以上値を送らない(すなわち、すべての送信ゴルーチンが終了したなど)場合、受信側のループは新しい値を待ってブロックされたままになり、プログラムが正常に終了しなくなるか、デッドロック状態になります。

例えば、以下のように読み込みだけ書いている場合、書き込みが来るまで待ち続けるためgoroutineリークが起きてしまう。

func main(){
	ch := make(chan int)
	go func ()  {
		fmt.Println(<-ch)
	}()
}

goroutineリークを確認する方法の1つにuberの外部パッケージがある。
テストファイルを作成し、以下のように記述。

main_test.go
package main

import (
	"testing"
	"go.uber.org/goleak"
)

func TestLeak(t *testing.T) {
	defer goleak.VerifyNone(t)
	main()
}

go test -v .を実行すると以下のようにFAILとなる。

--- FAIL: TestLeak (0.45s)
FAIL
FAIL    go-basic        0.926s
FAIL

先ほどのmain関数にch <- 10とチャネルに書き込みをしてからテストを再度実行するとテストはパスするようになる。

producerとconsumer

  • producerの結果をchannelに入れて、それをconsumerから取り出す
func producer(ch chan int, i int){
    // something
    ch <- i * 2
}

func consumer(ch chan int, wg *sync.WaitGroup){
    for i := range ch{
        fmt.Println("process", i * 1000)
        wg.Done()
    }
}
func main(){
    var wg sync.WaitGroup
    ch := make(chan int)

    // producer
    for i = 0; i < 10; i++ {
        wg.Add(1)
        go producer(ch, i)
    }

    // consumer
    // producerでchに追加された値を取得し、出力
    go consumer(ch, &wg)
    wg.Wait()
    close()
}

fan-out fan-in

  • 出力した値をchannelに格納し、それを取り出し、処理し、別のchannelに格納し...を繰り返す
func producer(first chan int){
    defer close(first)
    for i := 0; i < 10; i++{
        first <- i
    }
}

func multi2(first <-chan int, second chan<- int){
    defer close(second)
    for i := range first {
        second <- i * 2
    }
}

func multi4(second chan int, third chan int){
    defer close(third)
    for i := range second {
        third <- i * 4
    }
}
func main(){
    first := make(chan int)
    second := make(chan int)
    third := make(chan int)

    go producer(first)
    go multi2(first, second)
    go multi4(second, third)

    for result := range third {
        fmt.Println(result)
    }
}

close()の補足説明

  • close()関数は、それ以上チャネルに値が送信されないことを示す。
    チャネルがclose()された後も、そのチャネルに残っているすべての値は引き続き受信可能。つまり、close()が「これ以上データがこない」という信号を送るだけで、即座にチャネルのデータを消去するわけではない。

  • 閉じられたチャネルからデータを読み取ると、チャネルに残っているデータがあればそれが返され、データがもうない場合はその型のゼロ値が返され

channelとselect

  • selectを使うことで複数のchannelを受信できるようになる。
func goroutine1(ch chan string){
    for {
        ch <- "packet_from_1"
        time.Sleep(1 * time.Second)
    }
}

func goroutine2(ch chan string){
    for{
        ch <- "packet_from_2"
        time.Sleep(1 * time.Second)
    }
}
func main(){
    c1 := make(chan string)
    c2 := meke(chan string)
    go goroutine1(c1)
    go goroutine2(c2)

    for {
        select {
         case msg1 := <-c1:
             fmt.Println(msg1)
         case msg2 := <-c2:
             fmt.Println(msg2)
         default:
             fmt.Println("deafult")
        }
    }
}

Default Selectionとfor break

例えば、以下のコードでreturnしているが、これはmain関数を抜けてしまう。

func main(){
    tick := time.Tick(100 * time.Millisecond)
    boom := time.after(500 * time.Millisecond)
    for {
        select {
        case <- tick:
            fmt.Println("tick.")
        case <- boom:
            fmt.Println("Boom!")
            return
        default:
            fmt.Println("   .")
            time.sleep(50 * time.Millisecond)
        }
    }
}

main関数を抜けずにforループを抜けたい場合にどうするべきか、以下の通り
例えば、以下のコードでreturnしているが、これはmain関数を抜けてしまう。boomで抜けるにはどうすれば良いか?

func main(){
    tick := time.Tick(100 * time.Millisecond)
    boom := time.after(500 * time.Millisecond)
    Outerloop: // 任意の名前をつけてコロンで指定する
        for {
            select {
            case <- tick:
                fmt.Println("tick.")
            case <- boom:
                fmt.Println("Boom!")
                break OuterLoop // ここで指定すれば抜けられる
            default:
                fmt.Println("   .")
                time.sleep(50 * time.Millisecond)
            }
        }
}

sync.Mutex

これは問題なく動作しそうだが、実は問題がある。
実行結果が、加算が実行されるタイミングによって最終的に1が表示される。これをデータレースと呼ぶ。

Go においてロックをかけるという動作は「ロックがかかっている間は、他のゴールーチンからは参照・変更・ロック取得ができなくなる」ということ。

package main

import (
	"fmt"
	"sync"
)

func main(){
	var wg sync.WaitGroup
	var i int
	wg.Add(2)
	go func(){
		defer wg.Done()
		i++
	}()
	go func(){
		defer wg.Done()
		i++
	}()
	wg.Wait()
	fmt.Println(i)
}

data raceのチェックをするためにはgo run -race hoge.goと実行すれば確認できる。
実行してみると以下のように1つのデータレースが見つかったと表示される。

2
Found 1 data race(s)
exit status 66

このデータレースはsync.Mutexを使って回避できる。
以下のようにsync.Mutexを作成し、実行の中でLockをかけ、deferでUnlockするようにしておけば良い。

package main

import (
	"fmt"
	"sync"
)

func main(){
	var wg sync.WaitGroup
	var mu sync.Mutex
	var i int
	wg.Add(2)
	go func(){
		defer wg.Done()
		mu.Lock()
		defer mu.Unlock()
		i++
	}()
	go func(){
		defer wg.Done()
		mu.Lock()
		defer mu.Unlock()
		i++
	}()
	wg.Wait()
	fmt.Println(i)
}

sync.RWMutex

ただ、mutexのデメリットとして、本来mutexではロックされた場合、unlockされるまでデータを読み込むことはできないことが挙げられる。

そこで、RWMutexを使うことで並行して読み取りを行うことができる。

例えば、以下のようにMutexを使用すると実行結果から分かる通り
lock=>unlockされてから、次のlockが表示されていることからも、unlockを待っていることがわかる。

package main

import (
	"fmt"
	"sync"
	"time"
)

func main(){
	var wg sync.WaitGroup
	var rwMu sync.Mutex
	var c int

	wg.Add(4)
	go write(&rwMu, &wg, &c)
	go read(&rwMu, &wg, &c)
	go read(&rwMu, &wg, &c)
	go read(&rwMu, &wg, &c)

	wg.Wait()
	fmt.Println("finish")

}

func read(mu *sync.Mutex, wg *sync.WaitGroup, c *int){
	defer wg.Done()
	time.Sleep(10 * time.Millisecond)
	mu.Lock()
	defer mu.Unlock()
	fmt.Println("read lock")
	fmt.Println(*c)
	time.Sleep(1 * time.Second)
	fmt.Println("read unlock")
}

func write(mu *sync.Mutex, wg *sync.WaitGroup, c *int){
	defer wg.Done()
	mu.Lock()
	defer mu.Unlock()
	fmt.Println("write lock")
	*c += 1
	time.Sleep(1 * time.Second)
	fmt.Println("write unlock")
}

上記コードの実行結果は以下の通り

write lock
write unlock
read lock
1
read unlock
read lock
1
read unlock
read lock
1
read unlock
finish

これをRWMutexを以下のように実行すると、unlockを待たずして読み取りが並行して行われていることがわかる。

package main

import (
	"fmt"
	"sync"
	"time"
)

func main(){
	var wg sync.WaitGroup
	var rwMu sync.RWMutex
	var c int

	wg.Add(4)
	go write(&rwMu, &wg, &c)
	go read(&rwMu, &wg, &c)
	go read(&rwMu, &wg, &c)
	go read(&rwMu, &wg, &c)

	wg.Wait()
	fmt.Println("finish")

}

func read(mu *sync.RWMutex, wg *sync.WaitGroup, c *int){
	defer wg.Done()
	time.Sleep(10 * time.Millisecond)
	mu.RLock()
	defer mu.RUnlock()
	fmt.Println("read lock")
	fmt.Println(*c)
	time.Sleep(1 * time.Second)
	fmt.Println("read unlock")
}

func write(mu *sync.RWMutex, wg *sync.WaitGroup, c *int){
	defer wg.Done()
	mu.Lock()
	defer mu.Unlock()
	fmt.Println("write lock")
	*c += 1
	time.Sleep(1 * time.Second)
	fmt.Println("write unlock")
}

以下が実行結果

write lock
write unlock
read lock
1
read lock
1
read lock
1
read unlock
read unlock
read unlock
finish

MutexとRWMutexの使い分け by ChatGPT

  • sync.Mutex: シンプルな排他制御を提供し、読み取りと書き込みの頻度が同程度の場合に適しています。
  • sync.RWMutex: 読み取り操作が多く、書き込み操作が少ない場合に適しており、読み取りの並行性を高めることができます。

atomic

sync/atomicパッケージは、Go言語で提供される低レベルの同期プリミティブを提供し、特定の変数操作をアトミックに実行するために使用されます。これにより、複数のゴルーチンから同時にアクセスされてもデータの一貫性を保つことができます。

アトミック操作とは?
アトミック操作は、割り込みやコンテキストスイッチが発生しても分割されない操作を意味します。つまり、アトミック操作は常に完全に実行されるか、全く実行されないかのどちらかです。この特性により、競合状態を防ぐことができます。

まあ簡単にまとめると数値を増やしたり、比較してスワップしたりなど低レベルの操作を独立して行えるように準備されているってこと。だから、複雑な操作とかはできないけど、軽量で高速。

アトミックな整数操作

atomic.AddInt32(&i, delta int32) int32: 
32ビット整数に値をアトミックに加算します。
atomic.LoadInt32(&i int32) int32: 
32ビット整数をアトミックに読み込みます。
atomic.StoreInt32(&i, val int32): 
32ビット整数にアトミックに値を格納します。
atomic.SwapInt32(&i, new int32) int32: 
32ビット整数の値を新しい値とアトミックに交換します。

アトミックなポインタ操作:
atomic.LoadPointer(&ptr unsafe.Pointer) unsafe.Pointer: 
ポインタをアトミックに読み込みます。
atomic.StorePointer(&ptr unsafe.Pointer, new unsafe.Pointer): 
ポインタにアトミックに値を格納します。
package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {
    var counter int32
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            atomic.AddInt32(&counter, 1)
        }()
    }

    wg.Wait()
    fmt.Println("Final Counter:", counter)
}

他にもatomic.CompareAndSwapInt32とかもある。

package main

import (
    "fmt"
    "sync/atomic"
)

func main() {
    var counter int32 = 42

    // Counterが42なら50に更新
    swapped := atomic.CompareAndSwapInt32(&counter, 42, 50)
    fmt.Println("Swapped:", swapped, "New Counter:", counter) // Swapped: true, New Counter: 50

    // Counterが42なら60に更新
    swapped = atomic.CompareAndSwapInt32(&counter, 42, 60)
    fmt.Println("Swapped:", swapped, "New Counter:", counter) // Swapped: false, New Counter: 50
}

パッケージ

  • 他のファイルで定義した関数を使用する
main.go
package main

import "project/mylib" // importする

func main(){
    s := []int{1, 2, 3, 4, 5}
    result := myLib.Average // 3
}
package mylib

func Average(s []int) int {
    total := 0
    for _, v := range s{ // rangeはsliceやmapなどのコレクションを反復処理する
        total += v
    }
    return int(total/len(s))
}

サードパーティのpackegeをインストール

  • コマンドでインストール
    go install github.com/~
  • パスは以下で確認、ここにインストールされている
echo $GOPATH
  • go getはgo1.17より非推奨となっており、go installを使用する

godoc

  • godocを以下コマンドでインストール
    go install golang.org/x/tools/cmd/godoc@latest

  • godocを試してみる
    go doc fmt Println
    すると、以下が表示された。

package fmt // import "fmt"

func Println(a ...any) (n int, err error)
    Println formats using the default formats for its operands and writes to
    standard output. Spaces are always added between operands and a newline
    is appended. It returns the number of bytes written and any write error
    encountered.

便利な標準パッケージ

time

https://pkg.go.dev/time
色々なパターンがあるみたい。中でもRFC3339はよく使われるので押さえておきたい。

const (
	Layout      = "01/02 03:04:05PM '06 -0700" // The reference time, in numerical order.
	ANSIC       = "Mon Jan _2 15:04:05 2006"
	UnixDate    = "Mon Jan _2 15:04:05 MST 2006"
	RubyDate    = "Mon Jan 02 15:04:05 -0700 2006"
	RFC822      = "02 Jan 06 15:04 MST"
	RFC822Z     = "02 Jan 06 15:04 -0700" // RFC822 with numeric zone
	RFC850      = "Monday, 02-Jan-06 15:04:05 MST"
	RFC1123     = "Mon, 02 Jan 2006 15:04:05 MST"
	RFC1123Z    = "Mon, 02 Jan 2006 15:04:05 -0700" // RFC1123 with numeric zone
	RFC3339     = "2006-01-02T15:04:05Z07:00"
	RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00"
	Kitchen     = "3:04PM"
	// Handy time stamps.
	Stamp      = "Jan _2 15:04:05"
	StampMilli = "Jan _2 15:04:05.000"
	StampMicro = "Jan _2 15:04:05.000000"
	StampNano  = "Jan _2 15:04:05.000000000"
	DateTime   = "2006-01-02 15:04:05"
	DateOnly   = "2006-01-02"
	TimeOnly   = "15:04:05"
)

実際に上に変換するにはFormatを使えば良い。

package main 
import (
    "fmt"
    "time"
)

func main(){
    t := time.Now()
    fmt.Println(t)
    fmt.Println(t.Format(time.RFC3339))

    // 他にも日時も取得できる。
    t.Year()
    t.Month()
    t.Day()
    t.Hour()
    t.Minute()
    t.Second()
}

regex

  • 正規表現
    MatchString
    FindString
    FindStringSubmatch
    などなど

Sort

sort.Ints()
sort.Strings()
sort.Slice
などなど

iota

  • 連番を作ってくれる
const (
    c1 = iota
    c2
    c3
)

func main(){
    fmt.Println(c1, c2, c3) // 0, 1, 2
}

context

contextの目的
Goのcontextパッケージの目的は、長時間実行されるプロセスやリソース消費の大きい操作を効率的に管理し、必要に応じてそれらをキャンセルまたはタイムアウトさせるためのメカニズムを提供することです。具体的には、以下のような状況で役立ちます。

  • キャンセルの伝播:
    親プロセスがキャンセルされた場合、関連するすべての子プロセスもキャンセルされるようにする。
    例: ユーザーが操作をキャンセルした場合、それに依存するすべてのバックグラウンド処理も停止する。

  • タイムアウトの設定:
    プロセスが指定された時間内に完了しない場合、自動的にキャンセルする。
    例: APIリクエストが一定時間内に応答しない場合、そのリクエストをキャンセルする。

  • デッドラインの設定:
    特定の時刻を過ぎたらプロセスをキャンセルする。
    例: 処理が指定のデッドラインまでに完了しない場合に停止する。

以下が、contextパッケージの関数を説明した表です。

関数 説明 返り値
context.Background() 空の基礎となるコンテキストを作成します。主に他のコンテキストを作成する際の親として使用します。 Context
context.TODO() 将来的にコンテキストを決定する必要があるが、まだ未定の場合に使用します。通常、コードの見通しを良くするために使用されます。 Context
context.WithCancel(parent Context) 親コンテキストに基づいて、新しいキャンセル可能なコンテキストを作成します。キャンセル関数を返し、これを呼び出すことでコンテキストをキャンセルします。 Context, CancelFunc
context.WithTimeout(parent Context, timeout time.Duration) 親コンテキストに基づいて、新しいタイムアウト可能なコンテキストを作成します。指定された時間が経過すると自動的にキャンセルされます。キャンセル関数も返します。 Context, CancelFunc
context.WithDeadline(parent Context, deadline time.Time) 親コンテキストに基づいて、新しいデッドライン付きのコンテキストを作成します。指定された時刻に達すると自動的にキャンセルされます。キャンセル関数も返します。 Context, CancelFunc
context.WithValue(parent Context, key, val interface{}) 親コンテキストに基づいて、新しいキー・バリューのペアを持つコンテキストを作成します。このキー・バリューのペアは子コンテキストに渡すことができます。 Context

これらの関数を使用することで、Goプログラムにおける非同期処理やキャンセル可能な操作をより柔軟に管理することができます。

例えば、以下の例では、100ミリ秒のタイムアウトを持つコンテキストctxを作成している。つまり、このcontextは100ミリ秒経過した時点でctx.Done()チャンネルが閉じられます。

// 第一引数に親のcontextを指定する

ctx, cancel := contenxt.WithTimeout(context.Background(), 100 * time.Millisecond)

// ctx:サブgoroutineの第一引数に渡す`context`
// cancel:cancel関数が返ってきて、親の関数で実行すると、サブgoroutineで走っているチャネルをクローズすることができる。

例えば、以下のようにcontext.WithTimeoutで100m秒で指定した場合、指定の時間が経過した時にctx.Doneは閉じられるチャンネルを返す。case <-ctx.Done()が発火するのは、ctx.Done()チャンネルが閉じたときのみです。ctx.Done()チャンネルが閉じていない場合、<-ctx.Done()の受信操作はブロックされるため、case <-ctx.Done()は発火しません。

つまり、チャンネルは受信を待ち続けてブロックされるから、そこを通過しないためcaseに該当しない。
一方でキャンセル等されると、受信完了しているためcaseに該当する。

<-ctx.Done()の受信操作が完了している場合(チャネルが閉じられている場合)は「true」とみなすことができ、受信操作がブロックされている場合(チャネルが閉じていない場合)は「false」とみなすことができます。

package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

func main(){
	var wg sync.WaitGroup
	ctx, cancel := context.WithTimeout(context.Background(), 100 * time.Millisecond)
	defer cancel()
	wg.Add(3)
	go subTask(ctx, &wg, "a")
	go subTask(ctx, &wg, "b")
	go subTask(ctx, &wg, "b")
	wg.Wait()
}

func subTask(ctx context.Context, wg *sync.WaitGroup, id string){
	defer wg.Done()
	t := time.NewTicker(500 * time.Millisecond)

	select {
	case <-ctx.Done():
		fmt.Println(ctx.Err())
		fmt.Println(ctx.Done())
	case <-t.C:
		t.Stop()
		fmt.Println(id)
	}
}

もう少し具体的なコードで確認してみる。

package main

import (
    "fmt"
    "time"
)

func longProcess(ctx context.Context, ch chan string){
    fmt.Println("run")
    time.Sleep(2 * time.Second)
    fmt.Println("finish")
    ch <- "result"
}

func main(){
    ch := make(chan string)
    ctx := context.Background()
    // 3秒経ったらタイムアウト
    cts, cancel := context.WithTimeout(ctx, 3 * time.Second) 
    // 1秒経ったらタイムアウト→longProcessで2秒スリープしているため、完了するためにctx.Doneとなる
    cts, cancel := context.WithTimeout(ctx, 1 * time.Second) 
    
    defer cancel()
    go func() {
        defer close(ch) // ゴルーチンの最後にチャネルをクローズ
        longProcess(ctx, ch)
    }()

    Outer:
        for {
            select {
                case <- ctx.Done //ctxがキャンセルされた時にDoneとなる
                    fmt.Println(ctx.Err())
                    break Outer
                case <- ch:
                    fmt.Println("success")
                    break Outer
            }
        }
}

ioutil

  • io関係の読み書き
  • ioutil.ReadFile ioutil.Write ioutil.ReadAllなど
package main
import (
    "bytes"
    "fmt"
    "io/ioutil"
)

func main() {
    content, err := ioutil.ReadFile("main.go")
    if err != nil{
        log.Fatalln(err)
    }

    fmt.Println(string(content))

    if err := ioutil.WriteFile("ioutil.go", content, 0666); err != nil{
        log.Fatalln(err)
    }

    r := bytes.NewBuffer([]byte("abc"))
    content, _ := ioutil.ReadAll(r)
    fmt.Println(string(content))
}

他にも

package main

import(
    "fmt"
    "io/ioutil"
)

// 汎用性: []byte はテキストデータだけでなく、バイナリデータも扱えるため、汎用性が高い。
// ファイル入出力: Goのファイル操作関数は []byte を扱うため、ファイルの読み書きが容易。
// パフォーマンス: []byte は可変であり、効率的に操作できる。
type Page struct{
    Title String
    Body  []byte
}

func (p *Page)save() error {
    filename := p.Title + ".txt"
    //  Writeが成功したらnilで、失敗したらerrorなので返り値はerrorとなる
    return ioutil.Write(filename, p.Body, 0600) 
}

func loadPage(title String)(*Page, error){
    filename := title + ".txt"
    body, err := ioutil.ReadFile(file_name)
    if err != nil{
        return nil, err
    }
    return body, nil
}

func main(){
    p1 := &Page{Title:"test", Body: []byte("This is a sample Page.")}
    p1.Save

    p2, _ := loadPage(p1.Title)
    fmt.Println(string(p2.Body))
}

ネットワーク関連のライブラリ

http

import (
    "net/http"
    "io/ioutil"
    )
    
func main(){
    resp, _ := http.Get("http://example.com")
    defer resp.Body.Close()
    body, _ := ioutil.readAll(res.Body)
}

endpointを作る

import (
    "net/http"
    "io/ioutil"
    "net/url"
    )
    
func main(){
    bsase, _ := url.Parse("http://example.com") //  これはok
    bsase, err := url.Parse("http://ex    ample.com") // これはerr

    reference, err := url.Parse("/test?a=1&b=2")

    endpoint := base.ResolveReference(reference).String()

    // リクエスト
    req, _ := http.NewRequest("GET", endpoint, nil)
    // postのbodyには、bytes.NewBuffer([]byte("password"))のように格納する
    // ヘッダーの追加
    req.Header.Add("If-None-Match", "Hogehoge")
    // クエリパラメータ
    q: = re.URL.Query()
    q.Add("c", "3") // クエリパラメータの追加

    // 実際のリクエスト
    var client *http.Client = &http.Client{}
    resp, _ := client.Do(req)
    body, _ := ioutil.ReadAll(resp.Body)
}

json.UnmarshalとMarshalとエンコード

  • json.Marshal は、Goのデータ型をJSON形式のバイトスライスに変換します。
  • json.Unmarshal は、JSON形式のバイトスライスをGoのデータ型に変換します。
func main(){
        type Person struct{
            Name string `json: "name"`
            Age int `json: age`
            Nicknames []string `json: nickname`
        }
        // jsonのデータはリテラル``で表現する
        b := []byte(`{"name": "maike", "age": 20, "nicknames":["a", "b", "c"]}`)

        // このjsonデータをstructにどうやって入れるか
        var p Person
        // jsonをPersonストラクトの変数pに格納する
        // Unmarshalは第一引数に指定したGoのデータ型を第二引数で指定したポインタへ保存してくれる
        if err := json.Unmarshal(b, &p); err != nil{
            fmt.Println(err)
        }

        fmt.Println(p.Name, p.Age, P.Nicknames) 
        // maike 20 [a b c] のように受け取ったjsonをstructにそのまま入れてくれる

        // 反対にGoのデータ型をjsonに変換したい場合はmarshalを使用する
        v, _ := json.Marshal(p)
        // {"Name": "mike", "Age"; 20, "Nicknames": ["a", "b", "c"]}
}

ただここで注意が必要なのがストラクトのキーがそのまま使用されてしまうので
それを回避するためにストラクト定義でjson時のキー名をjson:hogeのように指定する必要がある。

他にもMarshal時の指定の仕方に-omitemptyなどもある。

type Person struct{
    Name string `json: "-"` // Marshalで含まない
    Age int `json:"age" string` // 型を指定してMarshal時に変更できる
    Nickname string `json:"nickname", omitempty` // 数値が0や文字列、スライスが空の場合に`-`と同じく含まない
}

あと、難しくて現時点では理解できなかったがUnmarshalJson()MarshalJson()でカスタマイズすることもできるらしい。

余談だがMarshalbyteで返却される。(Unmarshalはerrがある場合のみ返却される。)

func Marshal(v any) ([]byte, error) {
	e := newEncodeState()
	defer encodeStatePool.Put(e)

	err := e.marshal(v, encOpts{escapeHTML: true})
	if err != nil {
		return nil, err
	}
	buf := append([]byte(nil), e.Bytes()...)

	return buf, nil
}

hmacでAPI認証

import (
    "crypto/hmac"
    "crypto/sha256"
)

func main(){
    const apiKey = "User1Key"
    const apiSecret = "User1Sercret"

    // バイトスライスにすることでエンコード方式に関係なく一貫したデータ処理が可能になる
    // HMACは、SHA-256ハッシュ関数と秘密鍵 apiSecret を使用して初期化される
    h := hmac.New(sha256.New, []byte(apiSecret)) 

    h.Write([]byte("data")) //上のバイトスライスに追加したいデータのバイトを書き込む

    // hex.EncodeToString(...):このバイトスライスを16進数表現の文字列に変換する
    // h.Sum(nil):HMACの内部状態に基づいて最終的なハッシュ値が計算される
    // nilを引数に渡すことで、追加のデータを加えずに現在のハッシュ値を計算する
    sign := hex.EncodeToString(h.Sum(nil))
}

要はhmacにハッシュ関数と秘密鍵を指定してHMACオブジェクトを初期化し、
さらに、Writeで追加情報を与えた後にmac.Sumでハッシュ値を生成して、hexで16進数に変換している。

もっとざっくりいうとhmacは入れ物みたいなもので、初期化した後にwriteで、この入れ物に追加して、最終的に入っている内部の値でハッシュ値を算出するってこと。

サードパーティーのパッケージ

Semaphore

  • 共有リソースへの同時アクセスを一定数以下に制限する手続き
import (
    "context"
    "fmt"
    "time"
    "golang.org/x/sync/semaphore"
)
// ここで何個同時にgoルーチンを走らせるか設定する
var s *semaphore.Weighted = semaphore.NewWeighted(1)
func long Process(ctx context.Content){
    // Acquireでgoルーチンをロックする
    if err := a.Acquire(ctx); err != nil{
        fmt.Println(err)
        return
    }
    // goルーチンを抜けるタイミングで開放する
    defer s.Release
    fmt.Println("Wait...")
    time.Sleep(1 * time.Second)
    fmt.Println
}

func main(){    
    ctx := context.TODO()
    go longProcess(ctx)
    go longProcess(ctx)
    go longProcess(ctx)
    time.Sleep(5 * time.Second)
}

さらに、Acquireのようにロックして他のgoルーチンに待たせるのではなくすでに走っている場合は他のgoルーチンはキャンセルしたい場合はs.TryAcquireとする。

import (
    "context"
    "fmt"
    "time"
    "golang.org/x/sync/semaphore"
)
// ここで何個同時にgoルーチンを走らせるか設定する
var s *semaphore.Weighted = semaphore.NewWeighted(1)
func long Process(ctx context.Content){
    // s.TryAcquire(1) は、セマフォの許可を1つ取得しようとする。
    // 成功すれば true を返し、失敗すれば false を返す。
    isAcquire := s.TryAcquire(1)
    
    // もし false を返した場合、他のゴルーチンがすでにリソースを保持しているため、メッセージを出力して処理を終了。
    if !isAcquire {
        fmt.Println("could not get lock")
        return
    }
    // goルーチンを抜けるタイミングで開放する
    defer s.Release
    fmt.Println("Wait...")
    time.Sleep(1 * time.Second)
    fmt.Println
}

func main(){    
    ctx := context.TODO()
    go longProcess(ctx)
    go longProcess(ctx)
    go longProcess(ctx)
    time.Sleep(5 * time.Second)
}

iniでConfigファイルを読み取る

config.ini
[web]
port = 8080

[db]
name = stockdata.sql
driver = sqlite3
main.go
package main

type ConfigList struct{
    Port      int
    DbName    string
    SQLDriver string
}

var Config ConfigList

// main関数の前に実行
func init(){
    cfg, _ := ini.Load("config.ini") //config.iniを読み込み
    Config = ConfigList{
        Port: cfg.Section("web").Key("port").MustiInt(),
        Port: cfg.Section("db").Key("port").MustString("example.sql"), // 空の場合の初期値を設定
        DbName: cfg.Section("db").Key("driver").String() //空の場合標準で空文字となる
    }
}

func main(){
    fmt.Printf(Config.Port)  //8080
    fmt.Printf(Config.DbName) //stockdata.sql
    fmt.Printf(Config.SqlDriver) //sqlite3
}

環境変数

.envファイルを作成

.env
GO_ENV=development
main.go
package main

import (
	"fmt"
	"os"

	"github.com/joho/godotenv"
)
func main(){
    // godotenvにはLoad以外にもメソッドがあるので要チェック
	godotenv.Load()
	environment_variable := os.Getenv("GO_ENV")
	fmt.Println(environment_variable)
}

この時点だとまだ外部パッケージは取得できていないのでgo mod tidyを実行する
そして、go run main.goを実行すると以下のようにdevelopmentと表示される

% go run main.go
development

SAM

SAM 雛形作成

aws configure  
brew tap aws/tap
brew install aws-sam-cli

sam init --runtime provided.al2 --name direcrory_name

Which template source would you like to use?
	1 - AWS Quick Start Templates
	2 - Custom Template Location
Choice: 1

Choose an AWS Quick Start application template
	1 - Hello World Example
	2 - Infrastructure event management
	3 - Multi-step workflow
	4 - Lambda Response Streaming
	5 - DynamoDB Example
Template: 1

Which runtime would you like to use?
	1 - aot.dotnet7 (provided.al2)
	2 - go (provided.al2)
	3 - graalvm.java11 (provided.al2)
	4 - graalvm.java17 (provided.al2)
	5 - rust (provided.al2)
Runtime: 2

作成したディレクトリに移動してmake build実行すると以下のように表示され、無事成功。

%make build   
sam build
Starting Build use cache                                                                                                                                                          
Cache is invalid, running build and copying resources for following functions (HelloWorldFunction)                                                                                
Building codeuri: /Users/fukudanaoto/Desktop/aws-udemy-bitcoin/hello-world runtime: provided.al2 metadata: {'BuildMethod': 'go1.x'} architecture: x86_64 functions:               
HelloWorldFunction                                                                                                                                                                
Workflow GoModulesBuilder does not support value "False" for building in source. Using default value "True".                                                                      
Running GoModulesBuilder:Build                                                                                                                                                 
Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

次に実行するには、sam local invoke ファンクション名で、ローカルで実行できる。
今回はtemplate.yamlに書かれているHelloWorldFunctionを実行してみる。

template.yaml
Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Metadata:
      BuildMethod: go1.x
    Properties:
      CodeUri: hello-world/
      Handler: bootstrap // probided.al2ではbootstrapとする
      Runtime: provided.al2 // Runtimeは2023年12月でprovided.al2を使う必要が出てきた
      Architectures:
        - x86_64

すると、以下のレスポンスが返ってきた。

START RequestId: 222222-1111-1111-1111-11111111 Version: $LATEST
END RequestId: 1111111-1111-1111-1111-11111111
REPORT RequestId: 1111111-1111-1111-1111-11111111	Init Duration: 1.31 ms	Duration: 359.22 ms	Billed Duration: 360 ms	Memory Size: 128 MB	Max Memory Used: 128 MB	
{"statusCode": 200, "headers": null, "multiValueHeaders": null, "body": "Hello, world!\n"}

試しにmain.goを書き換えて、make build sam local invoke HelloWorldFunctionを実行すると

main.go
package main

import (
	"fmt"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)

func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
	name := "Bitcoin"
	return events.APIGatewayProxyResponse{
		Body:       fmt.Sprintf("Hello, %v", name),
		StatusCode: 200,
	}, nil
}

func main() {
	lambda.Start(handler)
}

以下のように返却される値が変わった。

{"statusCode": 200, "headers": null, "multiValueHeaders": null, "body": "Hello, Bitcoin"}

utils HTTPリクエスト

まずはutilsディレクトリをディレクトリをルートディレクトリ配下に作成し、中にhttp_utils.goを作成

package utils

メモ

  • 一般的な慣習として、main.go以外は、ディレクトリ名とパッケージ名は一致させる慣習
  • 同じパッケージであればファイルが違っても関数名や変数などは重複できない
buy-btc/bitflyer/product_code.go
package bitflyer

type ProductCode int

const (
    Btcjpy ProductCode = iota // 0
    Ethjpy                    // 1
    Fxbtcjpy                  // 2
    Ethbtc                    // 3
    Bchbtc                    // 4
)

func (code ProductCode) String() string {
    switch code {
    case Btcjpy:
        return "BTC_JPY"
    case Ethjpy:
        return "ETH_JPY"
    case Fxbtcjpy:
        return "FX_BTC_JPY"
    case Ethbtc:
        return "ETH_BTC"
    case Bchbtc:
        return "BCH_BTC"
    default:
        return "Unknown Product"
    }
}
buy-btc/bitflyer/bitflyer.go
package bitflyer

import (
	"encoding/json"
	"buy-btc/utils"
)

const baseURL = "https://api.bitflyer.com"
const productCodeKey = "product_code"

func GetTicker(code ProductCode) (*Ticker, error){
	url := baseURL + "/v1/ticker"
	query := map[string]string{productCodeKey: code.String()}
	body, err := utils.DoHttpRequest("GET", url, nil, query, nil)
	if err != nil{
		return nil, err
	}

	var ticker Ticker

	err = json.Unmarshal(body, &ticker)
	if err != nil{
		return nil, err
	}
	return &ticker, nil
}
// json:の後ろに半角スペースはだめ
type Ticker struct {
	ProductCode 		string  `json:"product_code"`
	State 				string  `json:"state"`
	TimeStamp			string  `json:"timestamp"`
	TickID				int     `json:"tick_id"`
	BestBid     		float64	`json:"best_bid"`
	BestAsk     		float64 `json:"best_ask"`
	BestBidSize         float64 `json:"best_bid_size"`
	BestAskSize         float64 `json:"best_ask_size"`
	TotalBidDepth       float64 `json:"total_bid_depth"`
	TotalAskDepth       float64 `json:"total_ask_depth"`
	Ltp     			float64 `json:"ltp"`
	Volume 				float64 `json:"volume"`
	VolumeByProduct     float64 `json:"volume_by_product"`
}
buy-btc/utils/http_utils.go
package utils

import (
	"bytes"
	"errors"
	"io" // ioutil/io で紹介されることが多いがgoの1.16時点ではioらしい
	"net/http"
)

func DoHttpRequest(method, url string, header, query map[string]string, data []byte) ([]byte, error) {
	if method != "GET" && method !="POST"{
		return nil, errors.New("GETとPOSTではありません。")
	}

	// httpRequest型でreqは以下のような形となる
	// &{POST https://example.com HTTP/1.1 1 1 map[] <nil> 9 [] key=value <nil> <nil> map[] false example.com map[] <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> 0xc000012340 <nil>}
	req, err := http.NewRequest(method, url, bytes.NewBuffer(data))
	if err != nil{
		return nil, err
	}

	// req.URL.Query() は、HTTPリクエストのURLに含まれているクエリパラメータを取得するためのメソッド
	// headerはマップ型だからkeyとvalueをAddでマップに追加する
	q := req.URL.Query()
	// Queryはマップ型
	for key, value := range query{
		q.Add(key, value)
	}


	// headerはマップ型だからkeyとvalueをAddでマップに追加する
	for key, value := range header{
		req.Header.Add(key, value)
	}

	// http.Client はHTTPリクエストを送信し、レスポンスを受信するために使用
	httpClient := &http.Client{}
	// このresはStatus, StatusCode, Proto(HTTP/1.1など), Header, Body, ContentLength, TLSなどのフィールドもある
	res, err := httpClient.Do(req)
	if err != nil{
		return nil, err
	}

	// res.Bodyはレスポンスデータが格納されるバッファ領域やネットワーク接続、ファイルディスクリプタを含むストリーム全体を指す
	defer res.Body.Close()

	// res.Body は io.ReadCloser 型です。これは、io.Reader と io.Closer インターフェースを組み合わせたものです。具体的には、以下のメソッドを持っています:
  // Read(p []byte) (n int, err error): データを読み取るためのメソッド。
  // Close() error: リソースを解放するためのメソッド。
	body, err := io.ReadAll(res.Body)
	if err != nil{
		return nil, err
	}

	return body, nil
}

// こんな感じでreqから取得もできる
// req.URL.Scheme // "https"
// req.URL.Host   // "example.com"
// req.URL.Path   // ""

// package main

// import (
//     "fmt"
//     "net/http"
//     "strings"
// )

// func main() {
//     req, err := http.NewRequest("POST", "https://example.com", strings.NewReader("key=value"))
//     if err != nil {
//         fmt.Println("Error creating request:", err)
//         return
//     }

//     // リクエストの詳細を表示
//     fmt.Println("Method:", req.Method)                  // "POST"
//     fmt.Println("URL:", req.URL)                        // "https://example.com"
//     fmt.Println("URL Scheme:", req.URL.Scheme)          // "https"
//     fmt.Println("URL Host:", req.URL.Host)              // "example.com"
//     fmt.Println("URL Path:", req.URL.Path)              // ""
//     fmt.Println("Header:", req.Header)                  // map[string][]string{}
//     fmt.Println("Body:", req.Body)                      // io.ReadCloser, 内容は "key=value"
//     fmt.Println("Host:", req.Host)                      // "example.com"
//     fmt.Println("Proto:", req.Proto)                    // "HTTP/1.1"
//     fmt.Println("ProtoMajor:", req.ProtoMajor)          // 1
//     fmt.Println("ProtoMinor:", req.ProtoMinor)          // 1
//     fmt.Println("ContentLength:", req.ContentLength)    // 9
//     fmt.Println("Close:", req.Close)                    // false
// }
main.go
package main

import (
	"fmt"
	"buy-btc/bitflyer"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)

func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
	// package名.メソッド
	// bitflyer.Bchbtc 0を呼び出している
	ticker, err := bitflyer.GetTicker(bitflyer.Bchbtc)

	if err != nil{
		return events.APIGatewayProxyResponse{
			Body:       "Bad Request",
			StatusCode: 400,
		}, nil
	}

	return events.APIGatewayProxyResponse{
		Body:       fmt.Sprintf("Ticker:%+v", ticker),
		StatusCode: 200,
	}, nil
}

func main() {
	lambda.Start(handler)
}

では、make build sam local invoke handlerNameを実行してみる。
以下の値が返ってきた!!

Init Duration: 0.95 ms	Duration: 412.06 ms	Billed Duration: 413 ms	Memory Size: 128 MB	Max Memory Used: 128 MB	
{"statusCode": 200, "headers": null, "multiValueHeaders": null, "body": "Ticker:&{ProductCode:BTC_JPY State:RUNNING TimeStamp:2024-05-17T06:49:14.223 TickID:15408842 BestBid:1.0278219e+07 BestAsk:1.0280887e+07 BestBidSize:0.7 BestAskSize:0.01 TotalBidDepth:371.50655758 TotalAskDepth:356.43436537 Ltp:1.028e+07 Volume:2867.28302373 VolumeByProduct:1349.95141561}"}

Lambda SAMデプロイ

以下、コマンドを実行する。

sam deploy --guided

StackName(CloudFormationで使用する名前)、AWS Regionなど色々と聞かれるが答えていくと完了する。

こんな感じでメッセージが表示されていればok

Successfully created/updated stack - udemy-citcoin in ap-northeast-1

テスト

testing.T 型の主要なメソッド

メソッド 説明
Error エラーメッセージを出力し、テストを失敗として記録します。
Errorf フォーマットされたエラーメッセージを出力し、テストを失敗として記録します。
Fail テストを失敗として記録しますが、テストの実行は続行します。
FailNow テストを失敗として記録し、テストの実行を即座に停止します。
Fatal エラーメッセージを出力し、テストを失敗として記録して、テストの実行を即座に停止します。
Fatalf フォーマットされたエラーメッセージを出力し、テストを失敗として記録して、テストの実行を即座に停止します。
Log ログメッセージを出力します。テストの結果には影響しません。
Logf フォーマットされたログメッセージを出力します。テストの結果には影響しません。
Helper 呼び出し元の関数をテストヘルパーとしてマークします。
Name 現在のテストの名前を返します。
Parallel テストを並行して実行できるようにマークします。
Skip テストをスキップし、スキップの理由を出力します。
SkipNow テストを即座にスキップし、スキップの理由を出力します。
Skipf フォーマットされたスキップの理由を出力してテストをスキップします。
Skipped テストがスキップされたかどうかを返します。
Run サブテストを実行します。

出力

関数名 機能 使用例
fmt.Errorf フォーマット指定子を用いてエラーメッセージを生成 err := fmt.Errorf("error: %s", msg)
fmt.Sprintf フォーマット指定子を用いて文字列を生成 str := fmt.Sprintf("value: %d", val)
fmt.Print 引数をそのまま出力 fmt.Print("Hello, World!")
fmt.Println 引数を出力し、改行を追加 fmt.Println("Hello, World!")
fmt.Printf フォーマット指定子を用いて出力 fmt.Printf("Name: %s, Age: %d", name, age)
log.Printf フォーマット指定子を用いてログに出力 log.Printf("Error: %v", err)
log.Println 引数をログに出力し、改行を追加 log.Println("Logging message")
log.Fatalf フォーマット指定子を用いてログに出力し、終了 log.Fatalf("Fatal error: %v", err)
errors.New 新しいエラーメッセージを生成 err := errors.New("an error occurred")
errors.Unwrap ラップされたエラーを取り出す err := errors.Unwrap(wrappedErr)
errors.Is エラーが特定のエラーと一致するかを判定 if errors.Is(err, specificErr) { ... }
errors.As エラーを特定の型に変換 if errors.As(err, &targetErr) { ... }
指定子 説明 使用例
%d 整数(10進数) fmt.Printf("%d", 123)
%s 文字列 fmt.Printf("%s", "Hello")
%f 浮動小数点数 fmt.Printf("%f", 1.23)
%g 浮動小数点数(短い形式) fmt.Printf("%g", 1.23)
%e 浮動小数点数(指数形式) fmt.Printf("%e", 1.23)
%t ブール値 fmt.Printf("%t", true)
%v デフォルトフォーマット fmt.Printf("%v", anyValue)
%+v 構造体のフィールド名付きデフォルトフォーマット fmt.Printf("%+v", struct{})
%#v Goの文法に従った値の表現 fmt.Printf("%#v", anyValue)
%T fmt.Printf("%T", 123)
%% リテラルの% fmt.Printf("%%")
%p ポインタのアドレス fmt.Printf("%p", &value)
%b 整数(2進数) fmt.Printf("%b", 5)
%o 整数(8進数) fmt.Printf("%o", 8)
%x 整数(16進数、小文字) fmt.Printf("%x", 255)
%X 整数(16進数、大文字) fmt.Printf("%X", 255)

使用例

package main

import (
	"fmt"
)

func main() {
	// 整数
	fmt.Printf("整数: %d\n", 123)          // 出力: 整数: 123

	// 文字列
	fmt.Printf("文字列: %s\n", "Hello")    // 出力: 文字列: Hello

	// 浮動小数点数
	fmt.Printf("浮動小数点数: %f\n", 1.23) // 出力: 浮動小数点数: 1.230000

	// ブール値
	fmt.Printf("ブール値: %t\n", true)     // 出力: ブール値: true

	// デフォルトフォーマット
	value := struct{ Name string }{"Alice"}
	fmt.Printf("デフォルトフォーマット: %v\n", value)  // 出力: デフォルトフォーマット: {Alice}

	// 型
	fmt.Printf("型: %T\n", 123)            // 出力: 型: int

	// ポインタ
	fmt.Printf("ポインタ: %p\n", &value)   // 出力: ポインタ: 0x1040c108

	// 2進数
	fmt.Printf("2進数: %b\n", 5)           // 出力: 2進数: 101

	// 8進数
	fmt.Printf("8進数: %o\n", 8)           // 出力: 8進数: 10

	// 16進数
	fmt.Printf("16進数: %x\n", 255)        // 出力: 16進数: ff
	fmt.Printf("16進数(大文字): %X\n", 255) // 出力: 16進数(大文字): FF

	// リテラルの%
	fmt.Printf("リテラルの%%\n")           // 出力: リテラルの%
}
1
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
1
1