ogatasoが何か書きます

SWE(1年目)が勉強したことを書くヨ

Golandでキーボードショートカットの設定を変更する

こんにちは、ogatasoと申します。普段はJetBrainsのIDEを主に使っていますが、より使いやすくするためにキーボードショートカットを変更したいと思いましたので、その方法をご紹介します。

今回変更したいショートカットについて

まずは、変更したいショートカットについてです。デフォルトのGoLand(Mac版)では、Command + F12を押すことでメソッドリストを表示できます。これは、コードが大きい場合に構造体やインターフェースが持つメソッドを素早く把握するのに非常に便利です。また、メソッドをクリックすることで、実際に定義されている場所にジャンプすることもできます。

メソッドリスト
しかし、Fnキーを押しながらでないと流れている音楽のボリュームが上がること、そしてF12がそもそもかなり遠い位置にあることで、なかなか押すのが面倒でした。今回はこれをCommand + Mに変更したいと思います。

変更方法

本題に入ります。

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

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を添付します。」とのことです。なので、最初の認識で大体あっている。

https://www.cloudflare.com/ja-jp/learning/privacy/what-are-cookies/#:~:text=Cookie%E3%81%A8%E3%81%AF%E3%80%81Web%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC,Cookie%E3%82%92%E6%B7%BB%E4%BB%98%E3%81%97%E3%81%BE%E3%81%99%E3%80%82

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ブラウザに保存されなかったり。軽く紹介するだけなので、以下を見た方が多分良い。

developer.mozilla.org

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が優先される。ちなみに無期限にすることはできないらしい。

Go言語のセンチネルエラーってなに?

初めてのGo言語を読んでいて、初めてみる「センチネルエラー」という単語に出会ったので、簡単に説明してみようと思います。

センチネルエラーとは、Goで時々見るエラーハンドリングのパターンの一つである(ちなみに、sentinelには見張りや衛兵といった意味があるらしい)。

単純にerr != nilでエラー判定するのではなく、err != zpi.ErrFormat のような比較をif文の条件にいれ、より具体的なエラーの種類を明示することで、特定エラーへの対応を行うことができる。

また、全てのセンチネルエラーはその変数の名前をErrから始める慣習がある。ただし、io.EOFは標準ライブラリの中では例外的にErrから始まらないセンチネルエラーである。

例えば、Goの標準ライブラリのarchive/apiではパッケージレベルで以下のようなエラーが定義されている。https://cs.opensource.google/go/go/+/refs/tags/go1.22.0:src/archive/zip/reader.go;l=27

var (
    ErrFormat       = errors.New("zip: not a valid zip file")
    ErrAlgorithm    = errors.New("zip: unsupported compression algorithm")
    ErrChecksum     = errors.New("zip: checksum error")
    ErrInsecurePath = errors.New("zip: insecure file path")
)

以下はその活用例(初めてのGo言語より)。

nonZipFile := bytes.NewReader(data)
_, err := zip.NewReader(nonZipFile, int64(len(data)))
if err == zip.ErrFormat {
    fmt.Println("ZIP形式ではありません")
}

こうしてみると、どのような種類のエラーが発生しうるのかが一目でわかってとても良さげ。というか、こういうのは他の言語でも似たようなものを見かけることはあるのでGo特有のエラーパターンではない気がする。

センチネルエラーのデメリットとして、柔軟性がなくなってしまうことが挙げられるため、その利用は必要最小限に抑えておくべきである。

Go言語でJWTを生成してみる

JWT認証はログインシステムの実装としてポピュラーですが、今回はGo言語でのJWTの生成にちょっとつまづいてしまったため、備忘録として書き残しておきます。以下のライブラリを利用してJWTを生成していきます。

github.com

JWTとは

初めに、JWTとは何かについて整理しておきます。JWTとはJson Web Tokenの略で、読み方は「ジョット」とからしいのですが、私はそのまま「ジェーダブリューティー」と呼んでしまっています。実物をお見せするとこんな感じです。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJncmVldGluZ3MiOiJIZWxsbywgV29ybGQhIn0.6Yh5DR_8xnjywt9K_ITKDip5r2wKa0_TYMFXJ5iXDaU

ピリオド(.)によって3つの部分に分かれていることを確認することができます。最初の部分はヘッダとして暗号アルゴリズムの名前およびトークンのタイプ(基本的には大文字で"JWT"と指定するのが推奨されている)が保存されており、真ん中の部分はペイロードが、それぞれBase64urlエンコード形式で保持されています。最後の部分はヘッダとペイロードを暗号化することで生成される署名になっています。JWTの中身は以下のようなデコーダーで確認することができます。 web-toolbox.dev

JWTはRFC 7519で仕様定義されているので、より詳しく知りたい方は読んでみるのも良いでしょう。 tex2e.github.io

JWTを作成する

JWTは以下のコードで生成できます。Claimsはペイロードに相当する部分で、jwt.NewWithClaimsでClaims付きでJWTを作ります。その後、token.SignedString([]byte("secret"))で署名部分を加えてJWTを完成させます。これはサンプルなので鍵を"secret"としていますが、本番運用では$ openssl rand -hex 32などで生成した乱数を利用するのが好ましいでしょう。

func createJWT() string {
    claims := jwt.MapClaims{
        "greetings": "Hello, World!",
    }
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    tokenWithSignature, _ := token.SignedString([]byte("secret"))
    return tokenWithSignature
}

JWTを検証、およびペイロードの中身を確認

jwt.Parseを利用してトークンをパースします。第二引数の関数にはパースされたトークンが渡され、初めにトークンの暗号方式が生成時に利用したjwt.SigningMethodHMACと同じものかどうかを検証したのち、鍵を渡しています。 ペイロードtoken.Claimsにあり、jwt.MapClaimsに型アサーションした上で中身を取ることができます(私はここがわかりにくくてつまづいていました...)。また、JWTの署名が正しいかどうかはtoken.Validで確認することができます(確認を忘れるとJWTの意味がなくなってしまうので要注意)。試しに以下のコードの"secret"を他の文字列に変えてみて、falseになっていればきちんとtoken.Validが機能していることを確認できます。

func verifyJWT(tokenString string) {
    token, _ := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
        }
        return []byte("secret"), nil
    })
    claims := token.Claims.(jwt.MapClaims)
    fmt.Println(claims["greeting"]) // Hello, world!
    fmt.Println(token.Valid) // true
}

脳に余裕をもたらす方法について 世界一流エンジニアの思考法 3章より

はじめに

世界一流エンジニアの思考法を読んでいるが、学びが非常に多い。3章の「脳に余裕を生む情報整理・記憶術」は特に自分に刺さりまくったのと、「学びをブログに書け!」というアドバイスが含まれていたので、早速3章の学びについて軽くまとめてみた。

3章概要

3章は主に脳の負担を軽減する方法について書かれている。具体的には、コードを素早く読む方法や記憶力を高める方法などである。私は頭の回転も記憶力もそれほど良くない方だと自己評価していることもあることから、全部の章の中でも3章はかなり有益だと感じた。

コードを早く読むには

私はコードリーディングには結構時間がかかる上、結局よくわからないままそれっぽい関数を見つけて雰囲気で作業しているが、筆者も同様の課題を抱えていたようである。結論としては、実装詳細についてはそれを書いたエンジニアを信用してコードは極力読まず、クラスの役割やインターフェイスなどに集中するのが良いそう。また、不明な点は人に聞くのが早い。(これができるのは多分きちんとしたアーキテクチャでソフトウェアが構築されていることが前提ではある。筆者はマイクロソフトで働いているので、かなりコードは綺麗なんだろうなぁ。)

タスクの選び方

タスクにはそれぞれ難易度があり、1. 調べずに解決できる, 2. ググりながら解決できる, 3. 解法を知らないがスパイクソリューションで解決できそう, 4. 自分一人では無理もしくはかなり時間がかかる の4つに大別できる。スパイクソリューションという単語は聞いたことがなかったが、情報収集を目的としてお試しでプログラムを作ってみるという手法らしい。1 筆者は「このレベル4の仕事ができるようにならねば」と焦っていたが、現在では生産性を上げるには2を1にしていくのを目指すのが良いと考えをあらためている。また、そうしていると3が2になることもあり得る。その他、作業をやっていてしんどくなるのはそれが自分のできるレベルを超えている可能性があり、自分のできるレベルを見積もることが大切であるとも記述されている。

アウトカム至上主義をやめる

自分もそうだが、1日の終わりに何かを成し遂げていないと「自分今日何もやってないじゃん!」と辛くなってしまう。ただ、実際には1つの作業は丁寧にやって、しっかりと理解することが長期的に見て生産性を向上させる。

マルチタスクをやめる

まずマルチタスクはやめた方がよく、4時間は自分の作業に集中する時間を取るべきとのこと。自分の時間を取るにはteamsやスラックを閉じれば良い。

記憶力を高める

記憶力の低さの原因は理解の浅さにある。理解を深める方法として以下のようなものが紹介されている。

  • 学んだことはブログに書く

  • コーネルメソッドで学びを整理し、しっかり復習する

  • 頭の中で整理し、言語化できるようにする。この時、実際にはやらないとしても後で人に説明することを意識しておくと効果が上がる。

  • ビジュアルイメージを構築する

物事をできるようになるためには理解、記憶、反復が必須である。理解していないとコントロールできないし、記憶していないと思い出すために時間を費やすことになってしまう。また、記憶、理解したことをいつでも取り出せるように反復できるようにしておくことも大切。これらが脳の負荷を軽減し、生産性を高めると言える。

Go言語のフルスライスについて

初めに

大前提として、Go言語では、スライスからスライスを切り出すことができる(配列からもスライスを切り出せる)。 スライスの基本的な切り出し方はx[i:j]であり、これにはi番目からj-1番目の要素が含まれている。

x := []int{1, 2, 3, 4}
y := x[:2]
z := x[1:]
d := x[1:3]
e := x[:]
fmt.Println("x:", x) // x: [1 2 3 4]
fmt.Println("y:", y) // y: [1 2]
fmt.Println("z:", z) // z: [2 3 4]
fmt.Println("d:", d) // d: [2 3]
fmt.Println("e:", e) // e: [1 2 3 4]

スライスを使う上での注意点

スライスからスライスを切り出す際にはデータのコピーを作っているわけでは無いので、要素を変更すると共有している全てのスライスが影響を受ける。

x := []int{1, 2, 3, 4} 
y := x[:2]  
z := x[1:]  
x[1] = 20
y[0] = 10
z[1] = 30
fmt.Println("x:", x) // x: [10 20 30 4]
fmt.Println("y:", y) // y: [10 20]
fmt.Println("z:", z) // z: [20 30 4]

Append干渉問題

また、スライスのスライスではキャパシティ部分も共有されるため、appendを使うとややこしいことになる。以下のコードでは、yはサイズ2, キャパシティ4に設定されるので、30をappendするとxの3が入っていた場所が上書きされてしまう。

x := []int{1, 2, 3, 4}  
y := x[:2]  
fmt.Println(cap(x), cap(y)) // 4 4 
y = append(y, 30)
fmt.Println("x:", x) // x: [1 2 30 4]
fmt.Println("y:", y) // y: [1 2 30]

解決法

appendの問題についてはフルスライス式という解決策がある。具体的には、キャパシティをスライスのスライスの長さに明示的に指定してあげることで、appendした際に新しくメモリを確保してくれて干渉を防ぐことができる。(とはいえ、フルスライス式はそんなに頻繁に出くわすものではなさそう。)

x := []int{1, 2, 3, 4}
y := x[:2:2]
fmt.Println(cap(x), cap(y)) // 4 4
y = append(y, 30)
fmt.Println("x:", x) // x: [1 2 3 4]
fmt.Println("y:", y) // y: [1 2 30]

参考文献:

qiita.com

isehara-3lv.sakura.ne.jp

Go言語のスライスについて調べる

Go言語では、配列を利用することは少なく、スライスを利用することが多い。なぜなら、スライスは「可変長の配列」のような役割を果たし、非常に便利であるからである。ここでは、Go言語のスライスについて調べていく。

スライスの中身

https://github.com/golang/go/blob/master/src/runtime/slice.go において、sliceは以下のように定義されている。

type slice struct { 
    array unsafe.Pointer 
    len int 
    cap int 
}

すなわち、sliceは配列をラップしたものであるということがわかる。

スライスの宣言

スライスの宣言方法は配列にかなり似ているが、重要な違いとしては長さを指定しないことだ。

var x = []int{1, 2, 3}
x := []int{1, 2, 3}
var x = []int{1, 2: 4, 15} // {1, 0, 4, 15}
// 多次元スライス
var x [][]int
var x []int // スライスのゼロ値、すなわちnilが初期値になる

スライスの比較

スライス同士は==や!=で比較することができず、比較できるのはnilだけである。nilとの比較でtrueとなるのはスライスの宣言だけを行なって具体的な値を代入していない状態で、空のスライスとnilを比較してもfalseになるので注意が必要である。なお、Go言語における nil は他の言語の null とは異なり、「型がない」という状態を意味する。

var x []int
var y = []int{}
fmt.Println(x == nil) // true
fmt.Println(y == nil) // false

スライス同士を直接比較できないことは不便に思えるかもしれないが、reflect パッケージの DeepEqual 関数を利用することで比較が可能となる。

x := []int{1, 2, 3}
y := []int{1, 2, 3}
fmt.Println(reflect.DeepEqual(x, y)) // true
y[0] = 1000
fmt.Println(reflect.DeepEqual(x, y)) // false

組み込み関数

len関数とcap関数

len 関数はスライスに含まれる要素の数を、cap 関数はスライスの容量を取得するために利用される。

x := []int{1, 2, 3}
fmt.Println(len(x)) // 3
fmt.Println(cap(x)) // 3

append関数

append関数を利用して、スライスに要素を追加できる。append(x, 5, 6, 7)のように、同時に複数の値も追加することも、演算子「...」を利用してスライスを展開し、マージすることもできる。

x := []int{5, 4}
append(x, 5, 6, 7)
var y = []int{3, 2, 1}
x = append(x, y...)

Go言語において、関数に引数を渡す場合、値のコピーが作成されてから渡される。このルールは、append関数にスライスを渡す場合にも適用される。つまり、append関数に渡されるのはスライスのコピーである。そのため、append関数はスライスのコピーに値を追加し、その結果を返す。

また、append関数を使用する際、スライスが参照する配列に十分なキャパシティがあれば、値の追加は高速に行われる。しかし、キャパシティが不足している場合は、新たな配列に十分なキャパシティを確保するために時間がかかる。このプロセスには、新しい配列の作成、元の配列の値のコピー、および新しい配列へのポインタの付け替えが含まれる。配列は固定長であるため、これらの手順が必要となる。

スライスのスライスおよび、フルスライスについてはこちらの記事で

参考文献

zenn.dev