Go言語でinterface型を色々な整数型にキャストする関数を作った
この記事ではGo言語において数値の入ったinterface型変数を整数型に変換するコードを紹介します。ジェネリクスを利用することで、関数一つで多くの整数型(int64, uint32など)へのキャストが可能になります。コードは自由に利用、改変、再配布していただいて構いませんが、正しい挙動は保証しません。また、float型からの変換は丸め誤差によって異なる値に変換される場合がありますので注意してください。
どういう時に必要か
まず、Go言語のinterface型は他の言語のany型のようになんでも代入することができる型です。なの、interface型のエイリアスとして、1.18からはanyが追加されていますので、そちらに読み替えてもらっても問題ないです。
Go言語の型システムはそこそこ厳しく、interface型からキャストする際に実態の型が判明している必要があります。
逆にいうと、実態を知っていれば型アサーションを利用してinterface型からその型に変換することができます。
例えば、以下のコードでma["num"]は確定でfloat64型になります。なぜならjson.Unmarshal関数ではjsonをinterface型へ変換する場合、数値はfloat64として保持されるからです。
参考: https://pkg.go.dev/encoding/json#Unmarshal
var ma map[string]interface{}
json.Unmarshal([]byte("{\"num\": 10}"), &ma)
fmt.Println(ma["num"], reflect.TypeOf(ma["num"])) // 10 float64
ということで、interface型変数の実際の型が事前に確定しているのであれば、この記事で紹介する関数は必要ありません。
ですが、実行まで型が確定しないという状況があります。例えば、以下の例ではxはfloat64とint64のどちらの可能性もあります。
var x any
if rand.Int()%2 == 0 {
x = float64(10.0)
} else {
x = int64(10)
}
コード
一言で書くと、型swtichで入力された値の型ごとに処理を分けて範囲チェックを行ったのちにキャストして返します
作成したコードは以下のようになります。
func InterfaceToInteger[T constraints.Signed | constraints.Unsigned](i interface{}) (T, error) {
var zero T
if i == nil {
return zero, nil
}
var maxValue = ^T(0)
var isSigned = maxValue < 0
if isSigned {
maxValue ^= 1 << (unsafe.Sizeof(maxValue)*8 - 1)
}
switch v := i.(type) {
case uint64:
if v > uint64(maxValue) {
return zero, fmt.Errorf("value out of range: %d", v)
}
return T(v), nil
case uint32:
if uint64(v) > uint64(maxValue) {
return zero, fmt.Errorf("value out of range: %d", v)
}
return T(v), nil
case uint16:
if uint64(v) > uint64(maxValue) {
return zero, fmt.Errorf("value out of range: %d", v)
}
return T(v), nil
case uint8:
if uint64(v) > uint64(maxValue) {
return zero, fmt.Errorf("value out of range: %d", v)
}
return T(v), nil
case uint:
if uint64(v) > uint64(maxValue) {
return zero, fmt.Errorf("value out of range: %d", v)
}
return T(v), nil
case int64:
if uint64(Abs(v)) > uint64(maxValue) || (!isSigned && v < 0) {
return zero, fmt.Errorf("value out of range: %d", v)
}
return T(v), nil
case int32:
if int64(Abs(v)) > int64(maxValue) || (!isSigned && v < 0) {
return zero, fmt.Errorf("value out of range: %d", v)
}
return T(v), nil
case int16:
if int64(Abs(v)) > int64(maxValue) || (!isSigned && v < 0) {
return zero, fmt.Errorf("value out of range: %d", v)
}
return T(v), nil
case int8:
if int64(Abs(v)) > int64(maxValue) || (!isSigned && v < 0) {
return zero, fmt.Errorf("value out of range: %d", v)
}
return T(v), nil
case int:
if int64(Abs(v)) > int64(maxValue) || (!isSigned && v < 0) {
return zero, fmt.Errorf("value out of range: %d", v)
}
return T(v), nil
case float64:
if Abs(v) > float64(maxValue) || (!isSigned && v < 0) {
return zero, fmt.Errorf("value out of range: %f", v)
}
return T(v), nil
case float32:
if float64(Abs(v)) > float64(maxValue) || (!isSigned && v < 0) {
return zero, fmt.Errorf("value out of range: %f", v)
}
return T(v), nil
default:
return zero, fmt.Errorf("unsupported type: %T", i)
}
}
func Abs[T ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~float32 | ~float64](x T) T {
if x < 0 {
return -x
}
return x
}
やっていることとしては、
- interface型変数
iを取得し、nilなら0を返す - 変更先の型Tの最大値を
maxValueに格納する iの型に応じて範囲チェックを行い、キャストして返す。ここでは type switch を利用している。
汎用性を高めるためにジェネリクスを使っています。これにより、一つの関数で様々な整数型へのキャストができるようになっています。コードではT ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64となっていますが、constraintsパッケージを利用するなら、T constraints.Signed | constraints.Unsignedとしても良いでしょう。その場合、AbsについてはT constraints.Signed | constraints.Floatですね。
また、以下の部分についてはunsafeを使わず、最大値を定数として定義しても良いかもしれません。
if isSigned {
maxValue ^= 1 << (unsafe.Sizeof(maxValue)*8 - 1)
}
一通りテストも書いてみました。
package main
import (
"testing"
)
func TestInterfaceToIntegerAllTypes(t *testing.T) {
tests := []struct {
Name string
input interface{}
expected interface{}
hasError bool
}{
// uint64への変換
{Name: "uint64 input as uint64", input: uint64(12345), expected: uint64(12345), hasError: false},
{Name: "uint32 input as uint64", input: uint32(54321), expected: uint64(54321), hasError: false},
{Name: "int64 input as uint64", input: int64(1000000000), expected: uint64(1000000000), hasError: false},
{Name: "float64 input as uint64", input: float64(1145141919811), expected: uint64(1145141919811), hasError: false},
{Name: "float32 input as uint64", input: float32(3.14), expected: uint64(3), hasError: false},
{Name: "negative int input as uint64", input: int(-100), expected: uint64(0), hasError: true},
{Name: "negative float64 input as uint64", input: float64(-123.45), expected: uint64(0), hasError: true},
{Name: "string input as uint64", input: "string", expected: uint64(0), hasError: true},
// int64への変換
{Name: "uint64 input as int64", input: uint64(12345), expected: int64(12345), hasError: false},
{Name: "int32 input as int64", input: int32(54321), expected: int64(54321), hasError: false},
{Name: "float64 input as int64", input: float64(987654321), expected: int64(987654321), hasError: false},
{Name: "negative int input as int64", input: int(-100), expected: int64(-100), hasError: false},
{Name: "negative float64 input as int64", input: float64(-123.45), expected: int64(-123), hasError: false},
// uint32への変換
{Name: "uint16 input as uint32", input: uint16(12345), expected: uint32(12345), hasError: false},
{Name: "int8 input as uint32", input: int8(127), expected: uint32(127), hasError: false},
{Name: "negative int input as uint32", input: int(-100), expected: uint32(0), hasError: true},
{Name: "large float64 input as uint32", input: float64(4000000000), expected: uint32(4000000000), hasError: false},
// int32への変換
{Name: "uint8 input as int32", input: uint8(255), expected: int32(255), hasError: false},
{Name: "int16 input as int32", input: int16(-12345), expected: int32(-12345), hasError: false},
{Name: "float32 input as int32", input: float32(21474836), expected: int32(21474836), hasError: false},
{Name: "large float64 input as int32", input: float64(2147483648), expected: int32(0), hasError: true},
{Name: "string input as int32", input: "string", expected: int32(0), hasError: true},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
var err error
var result interface{}
switch tc.expected.(type) {
case uint64:
result, err = InterfaceToInteger[uint64](tc.input)
case int64:
result, err = InterfaceToInteger[int64](tc.input)
case uint32:
result, err = InterfaceToInteger[uint32](tc.input)
case int32:
result, err = InterfaceToInteger[int32](tc.input)
default:
t.Fatalf("Unexpected type: %T", tc.expected)
}
if tc.hasError {
if err == nil {
t.Errorf("Expected an error but got none")
}
} else {
if err != nil {
t.Errorf("Unexpected error: %v", err)
} else if result != tc.expected {
t.Errorf("Unexpected result: got %v, want %v", result, tc.expected)
}
}
})
}
}
使用例
最後に使用例を乗っけて終わります。以下はuint64をint32に変換しています。
var x interface{}
x = uint64(12345)
var y = InterfaceToInteger[int32](x)
fmt.Println(y, reflect.TypeOf(y)) // 12345 int32
Go言語のカンマokイディオムって何?
「初めてのGo言語」を読んでいて出てきたカンマokイディオムについて簡単に説明します。mapのカンマokイディオムは有名ですが、型アサーションやチャネルでも利用可能です。
map
カンマokイディオムを利用することで、mapの「ゼロ値と結び付けられているキー」と「マップに存在しないキー」を区別することが可能です。具体例を挙げると以下のようになります。
m := map[string]int{"hoge": 0}
v, ok := m["hoge"]
fmt.Println(v, ok) // 0 true
v, ok = m["fuga"]
fmt.Println(v, ok) // 0 false
okの値は、セットされているキーの場合はtrue, されていない場合にはfalseとなっています。二番目の演算子を, okで受け取るので、カンマokイディオムと言われているんですね。
型アサーション
型アサーションはインターフェース型あるいはany型の値が特定の型であるかをチェックする方法です。もう少しわかりやすく説明すると、interface{}やanyの変数を型アサーションを使ってその型のメソッドやその型を受け取る関数を利用できる状態にします。 型アサーションは実際の値と型が異なっていた場合、以下のようにパニックを起こしてしまいます。
var x any = "hello"
y := x.(int) // panic: interface conversion: interface {} is string, not int
カンマokイディオムを利用すれば、パニックが起こらないようにしつつ変数が型アサーションで指定した型を満たしていたのかを確認することができます。
var x any = "hello" y, ok := x.(int) fmt.Println(y, ok) // 0 false
コンテキストのValueメソッドとの組み合わせで使うのも頻出だと思います。
userID, ok := ctx.Value(ctxKeyUserID).(string)
チャネル
Go言語のチャネルは、すでにクローズされたチャネルから値を読み込もうとした場合、エラーにはならず常にゼロ値が返されます。ゼロ値が返ってきた理由が「ゼロ値が書き込まれたからなのか」、「チャネルがクローズされていたからなのか」を判別するためカンマokイディオムを利用することができます。falseが返っていれば、チャネルがクローズされていることがわかります。
v, ok := <-ch // 0, false
参考文献:
2024年前半振り返り 新卒ソフトウェアエンジニア
2024年も7月に入り、早くも半分が終わってしまったので振り返っていきます。
大学院を卒業した
無事に修士課程を修了しました。修士論文はしんどかったですが、割と早めに手をつけていたこともあり、後半は思ったよりもゆったりとしていました。また、去年12月にMDPIに論文を1本出せていたことで、後期授業料が返還されることになったのがめちゃくちゃ嬉しかったです。春休みは個人開発やったり本を読んだりしていました。
社会人になった
東京の某IT企業に入社したのですが、基本的には広島からリモートワークです。広島から東京は新幹線でおよそ4時間かかるので、たまにある出社はとてもしんどいです。ちなみに、同期は全員弊社でインターンをバリバリやってたこともあり、新卒らしい新卒は僕だけです。研修ではおんぶに抱っこでした。以下イメージ図
体力がないので最初は8時間の労働めっちゃ長くてしんどかったのですが、最近ちょっとだけ慣れてきました。嘘です。しんどい日も結構あります。残業はほとんどしていないですが、早く成長するにはやったほうが良いのかなと思ったり思わなかったり。
仕事は順調なのかと問われると、まだキャッチアップ期間であんまり戦力になれている感覚はないです。課題は山積みですねぇ。使用期間でクビにならなかったので一安心です。
新NISAを始めた
研修でなぜかNISAを勧められたのでその日のうちにオルカンにぶち込みました。貯金あったのを一括で入れたほかに、毎月10万積み立てています。銘柄はオルカンとTOPIXで、今のところはオルカンの方がパフォーマンスが良いですが、日本には底力があると考えており、底力だけに海底資源とか...(遠い目)。とりあえずの目標は3000万貯めてアッパーマス層の仲間入りすることですね。一体いつになるやらって感じなんですが、私は子供部屋おじさんなのでいける気がします。
読んだ本
いくつか読んだのですが、その中でも特に良かった本を紹介します
初めてのGo言語
弊社がGo言語を採用しているので入社前に読んでおきました。言語仕様であったり、イディオムであったりが読みやすくまとめられており、入門書として文句なし、大満足です。初めてのGo言語に関してはいくつかブログに記事を書いていますし、勉強ノートもGithubに書いてたりするので気になるのであればチラ見していただけると(14章途中で力尽きていますが笑)
Googleのソフトウェアエンジニアリング
これも非常に良い本で、前半はソフトウェアエンジニアリングとはなんぞやというところから、どのような心持ちで働くのが良いのか詳しく書いています。後半はGoogleのような非常に規模の大きい企業がどのように回っているのかを解説しています。個人的には前半部分の特に2章が全人類(クソデカ主語)にとって読むべき内容で、相談やフィードバック、小さな人付き合いの重要性、失敗と振り返りの価値などチームで働く際に絶対に頭に入れておくべき知識が詰まっています。
世界一流エンジニアの思考法
この本は米マイクロソフトのシニアソフトウェアエンジニアの牛尾 剛さんが書かれた本で、彼の周りにいる技術イケメン達がどのような考え方や習慣を持っているかをまとめています。失敗の重要性を説いているのはGoogleのソフトウェアエンジニアリングと被る部分もありますが、理解することの重要性であったり、そのほかに情報整理術や記憶術等にも触れられています。
上の二つの本よりはカロリー低めで読みやすく、寝る前とかにちょっと読むとかにちょうど良さそうな気がします。
買って良かったもの
Macbook Air(M3)
大学院時代に借りていたMacbookはきちんと返し、会社から貸与されているPCは基本的に業務時間外に開いたらダメっぽいので仕方なく購入しました。今のところ期待通りの働きをしてくれています。色はミッドナイトを選んだのですが、かっちょいいです。
Shokzの骨伝導イヤホン
なんでもっと早く買わなかったんやって後悔しているものが骨伝導イヤホンのOpen Runです。耳を塞がないので、周りの音が聞こえる、耳が痛くなりづらい、お風呂上がりの湿った耳に詰め物をしなくて済む等々メリットが多いです。音質も十分です。
旅行
引きこもりなので、どこにも行ってないです。行きたい場所も特にないです。
まとめ
個人的には結構濃い2024年前半でした。後半も来年の正月くらいに書くと思います。
jq入門(問題付き)
jqとは
jqはjsonから特定の値を抽出するのに超絶便利なコマンドラインツールです。テキスト処理を行えるCLIツールとしてはsedやawkなどいろいろとありますが、JSONの処理においてはjqの右に出るものは現状いないという認識です。 本記事では、問題を交えながら実践形式でjqの基本を抑えていきます。
インストール
Macユーザでjqコマンドのインストールがまだの方は以下の方法でインストールすることが可能です。 jqはその他のOSでも利用可能ですので、Mac以外のユーザは他サイトを参考にインストールしていただけたらなと思います。
brew install jq
brewコマンドを使えなくて失敗する方は先にHomebrewをインストールする必要があります。
以下のコマンドでjqコマンドが使えることを確認しましょう。
jq --version
基本的な使い方
jqは基本的に以下のようにcurlやcatを利用してjsonの出力し、パイプを利用してjqコマンドに流し込むといった利用方法になります。
cat sample.json | jq curl "http://localhost:7000" | jq
では早速問題を出します。
Q1 https://jsonplaceholder.typicode.com/photos をcurlで叩いて、jqで表示してみてください。 以下の「答えを見る」を押すとコマンドが表示されます。
答えを見る
curl "https://jsonplaceholder.typicode.com/photos" | jq
抽出
jqの利用方法で最も多いであろう、jsonデータからの値の抽出を見ていきます。
jqでは jq 'コマンド' の形式でjsonから必要なデータのみを抽出することが可能です。
また、jq 'コマンド1 | コマンド2'のようにパイプで繋げることで複数のコマンドを適用することもできます。
配列
https://jsonplaceholder.typicode.com/photos から帰ってくるデータは配列の中に複数の要素が含まれている形式になっていました。
配列の抽出では .[n] を使ってn番目の要素をとったり、 .[n:m] で n以上m未満の範囲をとってくることができます。
また、lengthと指定すると要素の数を調べることができます。
以下の問題でも https://jsonplaceholder.typicode.com/photos を使って回答してください
Q2 0番目の要素のみを出力してください
# 期待される出力
{
"albumId": 1,
"id": 1,
"title": "accusamus beatae ad facilis cum similique qui sunt",
"url": "https://via.placeholder.com/600/92c952",
"thumbnailUrl": "https://via.placeholder.com/150/92c952"
}
答えを見る
curl "https://jsonplaceholder.typicode.com/photos" | jq '.[0]'
Q3 2番目から4番目までの要素のみを出力してください
[
{
"albumId": 1,
"id": 3,
"title": "officia porro iure quia iusto qui ipsa ut modi",
"url": "https://via.placeholder.com/600/24f355",
"thumbnailUrl": "https://via.placeholder.com/150/24f355"
},
{
"albumId": 1,
"id": 4,
"title": "culpa odio esse rerum omnis laboriosam voluptate repudiandae",
"url": "https://via.placeholder.com/600/d32776",
"thumbnailUrl": "https://via.placeholder.com/150/d32776"
},
{
"albumId": 1,
"id": 5,
"title": "natus nisi omnis corporis facere molestiae rerum in",
"url": "https://via.placeholder.com/600/f66b97",
"thumbnailUrl": "https://via.placeholder.com/150/f66b97"
}
]
答えを見る
.[n:m]で指定した時、m番目の要素は含まないことに注意してください。
curl "https://jsonplaceholder.typicode.com/photos" | jq '.[2:5]'
Q4 配列の要素数を出力してください。
# 期待される出力 5000
答えを見る
curl "https://jsonplaceholder.typicode.com/photos" | jq 'length'
キーによる抽出
キーに対応する値を求める時には.キーという形式が利用できます。
また、複数の値を出したい場合は、カンマ(,)を使って.キー1,.キー2のように表現することができます。
配列要素から取得する場合はjq '.[n].key'のような指定が可能です。もしくは、パイプを利用して、jq 'コマンド1 | コマンド2'のようにすることもできます。
Q5 0番目の要素のtitleを出力してください。
# 期待される出力 "accusamus beatae ad facilis cum similique qui sunt"
答えを見る
curl "https://jsonplaceholder.typicode.com/photos" | jq '.[0].title' # パイプを利用した別解 curl "https://jsonplaceholder.typicode.com/photos" | jq '.[0] | .title'
Q6 0番目の要素のtitleだけでなく、idとalbumIdも同時に出力してください。
# 期待される出力 "accusamus beatae ad facilis cum similique qui sunt" 1 1
答えを見る
curl "https://jsonplaceholder.typicode.com/photos" | jq '.[0].title,.[0].id,.[0].albumId' # パイプを利用した別解 curl "https://jsonplaceholder.typicode.com/photos" | jq '.[0] | .title,.id,.albumId'
Q6ではidとalbumIdのどちらも整数値なので、出力結果をみた時にどちらがidでどちらがalbumIdなのかわかりにくいですよね。 キーとバリューを含んだJSON形式で出力するには中括弧({})を利用します。
以下はその例です。
curl "https://jsonplaceholder.typicode.com/photos" | jq '.[0] | {id: .id, albumId: .albumId}'
# 出力
{
"id": 1,
"albumId": 1
}
また、同じ要領で角括弧([])を使って配列を作ることもできます。
curl "https://jsonplaceholder.typicode.com/photos" | jq '.[0] | [.id, .albumId]' # 出力 [ 1, 1 ]
Q7 0番目の要素のタイトルとurlから構成されるJSONを出力してみてください。
# 期待される出力
{
"title": "accusamus beatae ad facilis cum similique qui sunt",
"url": "https://via.placeholder.com/600/92c952"
}
答えを見る
curl "https://jsonplaceholder.typicode.com/photos" | jq '.[0] | {title: .title, url: .url}'
配列に対する便利関数
ここからの問題では以下の内容のsample.jsonを使います。
[
{
"id": 1,
"name": "Alice",
"age": 30
},
{
"id": 2,
"name": "Bob",
"age": 25
},
{
"id": 3,
"name": "Charlie",
"age": 35
}
]
配列に対してはサイズを取得するlengthの他に、最大最小を求めるmaxやmin、並びを変えるsortなど使いこなせると便利な機能がたくさんあります。
今回はサンプルとして配列サイズ小さなjsonファイルを利用していますが、大きなjsonファイルを扱う時にはかなり強力です。
また、map(.key)を利用すると、配列の要素を絞り込むことができます。
Q9 それぞれの要素の年齢のみを配列として抽出してください
# 期待される出力 [ 30, 25, 35 ]
答えを見る
cat sample.json | jq 'map(.age)'
Q10 最も高い年齢を出力してください。
# 期待される出力 35
答えを見る
cat sample.json | jq 'map(.age) | max'
max_by(.key)やmin_by(.key)を利用するとあるフィールドの最大値、最小値をもつオブジェクトを検索して出力することができます。
Q11 最も若い人の名前を表示してください。
# 期待される出力 "Bob"
答えを見る
cat sample.json | jq 'min_by(.age).name'
まだまだいろいろあるんですがここまでにします。もっとたくさんの機能を知るには以下の記事がおすすめです。
参考:
トランザクション分離レベルと問題
トランザクション分離レベルと、各レベルで出てくる問題を整理する。
トランザクション分離レベル
まず、トランザクション分離レベルは4段階あり、「READ UNCOMMITED」→「READ COMMITED」→「REPEATABLE READ」→「SERIALZABLE」の順にトランザクションの独立性が高くなっていく。
じゃあSERIALIZABLEにすれば良いのではと思われるかもしれないが、一般にパフォーマンスとのトレードオフであることに注意を払う必要がある。
トランザクション分離レベルと発生しうる問題
以下ではトランザクション分離レベルの簡単な説明と、そのレベルでどのような問題が起きるかを解説していく。上のレベルで発生する問題は下のレベルでも発生する。
SERIALIZABLE
SERIALIZABLEは最も高い分離レベルであり、トランザクションは直列化可能である。直列化可能ということは複数のトランザクションが実行されていてもそれぞれが順番に実行されたように見えるということであり、トランザクションは実行中の他のトランザクションに影響を与えることはない。SERIALIZABLEは特に独立性に関する問題を起こしません。
REPEATABLE READ
REPEATABLE READは2番目に高いトランザクション分離レベルである。同一トランザクション内で複数回の読み込みを行った時に他のトランザクションによって追加、削除されたデータが混入することがある。これをファントムリードと呼ぶ。MySQLのデフォルトの分離レベルはこれである。なお、MySQLではMVCC(MultiVersion Concurrency Control)によってファントムリードを防ぐことができる。
MVCCとは
簡単に説明すると自分より後に発行されたトランザクションの変更を読み取らないようにするために以下の2つを行っている。 -レコードにトランザクションIDを記録する - 更新、削除されるデータを取っておいて後からでも参照できるようにする。
以下が分かりやすかった。 sairoutine.hatenablog.com
READ COMMITTED
3番目に高いトランザクション分離レベル。このレベルではコミットされていないトランザクションの読み取りからは保護されるが、ノンリピータブルリード(ファジーリードとも呼ばれる)が発生する。ノンリピータブルリードでは同一トランザクションで同じレコードを複数回読んだ時に異なる結果が返ってくることがある。
READ UNCOMMITTED
最も低い分離レベルである。コミットされていない変更を他のトランザクションから読みとれてしまう(ダーティリード)。
Golandでキーボードショートカットの設定を変更する
こんにちは、ogatasoと申します。普段はJetBrainsのIDEを主に使っていますが、より使いやすくするためにキーボードショートカットを変更したいと思いましたので、その方法をご紹介します。
今回変更したいショートカットについて
まずは、変更したいショートカットについてです。デフォルトのGoLand(Mac版)では、Command + F12を押すことでメソッドリストを表示できます。これは、コードが大きい場合に構造体やインターフェースが持つメソッドを素早く把握するのに非常に便利です。また、メソッドをクリックすることで、実際に定義されている場所にジャンプすることもできます。

変更方法
本題に入ります。
まずはCommand + , を押して設定画面を表示します。キーボードショートカットに関する設定はKeymapという項目から変更できます。

今回のコマンドはMain Menu -> Navigate -> Go to by Reference Actions -> File Structureにありました。
右クリックからAdd Keyboard Shortcutを押し、Command + Mを設定すれば完了です。やったー🙌

Cookieについて整理する
最近、Go言語で個人開発をやっていてCookieでつまづいたことで、Cookieに対する理解度の低さを改めて感じたので少し調べていきます。
執筆前の僕のCookieの認識は「ブラウザで保存されるデータで、http通信で送信されてログインとかに利用されるやつ」なので、間違っていたら教えてください。
定義
Cloudflareの定義によると「Cookieとは、Webサーバーが生成してWebブラウザーに送信する小さな情報ファイルです。Webブラウザは、受け取ったCookieを所定の期間、またはWebサイト上にユーザーのセッションがある間保存します。また、ユーザーは次回以降Webサーバーにリクエストする際に、関連するCookieを添付します。」とのことです。なので、最初の認識で大体あっている。
Cookieとは? | Cookieの定義 | Cloudflare
Cookieを保存する流れ
サーバーが発行したJWTなどをCookieに保存する場合、サーバーはHTTPリクエストのヘッダに"Set-Cookie"というフィールドを含み、その中でkey=value;の形で保存して欲しいCookieを指定することができる。また、「;」の後には属性が続いていく。複数のCookieを設定する場合、それぞれのCookieに対して別々のSet-Cookieフィールドが含まれる。つまり、3種類のキーバリューを保存して欲しい場合はSet-Cookieフィールドを3つ指定する。
ちなみにGo言語で設定するならhttpライブラリを使ってこんな感じになる。
http.SetCookie(w, &http.Cookie{
Name: "jwt",
Value: tokenString,
Secure: true,
HttpOnly: true,
Path: "/",
Expires: time.Now().Add(60 * 60 * 24 * time.Minute),
SameSite: http.SameSiteNoneMode,
})
一方で、クライアントがサーバーにリクエストを送る際には"Cookie"というフィールドにキーバリューを詰めて送る。ここで、キーバリューは「;」で分けられる。
属性
この属性がよくわからなかったので調べていく。属性次第ではSet-Cookieを指定してもWebブラウザに保存されなかったり。軽く紹介するだけなので、以下を見た方が多分良い。
Secure
HTTPSでのみ利用できるようにする。今時基本的にHTTPSなので指定しておいて損はない気がする。ローカルホストだと動かなくなるので注意。 (追記: ブラウザによってはlocalhostでのSecure属性は無視してくれるらしい。やってみるとchromeはそうっぽい。)
teratail.com
HttpOnly
この属性が指定されたクッキーはJavaScriptから読まれない。XSS攻撃の対策になる。JSで利用しないならtrueにしておいてよさそう。
Domain
Cookieを受信できるホストを指定する。指定しない場合はサーバと同じドメインをホストとし、サブドメインは除外される。指定した場合はサブドメインも含まれる。例えば、Domain=mozilla.org を設定すると、developer.mozilla.org のようなサブドメインも含まれる。
Path
この属性が指定されると、Pathに一致するページでのみリクエストにクッキーが含まれる。 "/"であれば全てのパスに一致するし、"/users"としたら"/message"には一致せず、クッキーは送信されない。
SameSite
SameSite属性では異なるサイト間でのリクエストでCookieを送るべきかそうでないかを指定できる。これは、CSRFに対しての防御となる。SameSiteの設定にはStrict, Lax, Noneの3種類がある。デフォルトはLax。
Strict ... 元サイトからのリクエストに対してのみCookieを送る。外部サイトからのリダイレクトを含む、他のサイトからのリクエストではCookieは送信されない。
Lax ... 元サイトに対してのみCookieを送るが、外部サイトからリンクを辿った場合にもCookieを送る
None ... 異なるサイト間でも送信できる(ただし、Secure属性を設定する必要がある。)
Expires および MaxAge
Cookieを残す期間を指定することができる。両方指定した場合はMaxAgeが優先される。ちなみに無期限にすることはできないらしい。





