はじめに
私自身は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 というファイルがある場所が、モジュールのルートディレクトリとしてみなされるようになります。
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フィールドを取得することもできる。
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)を取得できるということ。
まあざっくり言えばポインタ型はメモリアドレスを格納しておく場所のようなもので、それを使えば値も取れるってこと。
ダブルポインタ
上記が理解できればダブルポインタもそんなに難しいことではない。
箱であるポインタに入れたメモリアドレスを、別の箱であるポインタに入れるってこと。マトリョーシカ的な感じだね。
*
だと1回のdereference
を行う。
つまり、ポインタ変数が保持しているメモリアドレスを元に、そのアドレスに格納されている値にアクセスできる
。
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は合流せずに終了してしまうこととなる。
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
の外部パッケージがある。
テストファイルを作成し、以下のように記述。
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
}
パッケージ
- 他のファイルで定義した関数を使用する
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()
でカスタマイズすることもできるらしい。
余談だがMarshal
はbyte
で返却される。(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ファイルを読み取る
[web]
port = 8080
[db]
name = stockdata.sql
driver = sqlite3
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
ファイルを作成
GO_ENV=development
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
を実行してみる。
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
を実行すると
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以外は、ディレクトリ名とパッケージ名は一致させる慣習
- 同じパッケージであればファイルが違っても関数名や変数などは重複できない
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"
}
}
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"`
}
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
// }
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") // 出力: リテラルの%
}