ゆとり日記

心にゆとりを持って生きたいプログラマーの雑記です。気が向いたら書きます。

Go1.22のfor文と変数スコープ

asakusago.connpass.com

asakusa.goで話すLTのネタとして調べていたメモが一定の分量になったので、ブログに書いておくことにした。

for文の中のクロージャ

import "fmt"

func Print123() {
    var prints []func()
    for i := 1; i <= 3; i++ {
        prints = append(prints, func() { fmt.Println(i) })
    }
    for _, print := range prints {
        print()
    }
}

func main() {
    Print123()
}

いきなりだが、このコードをGo 1.21以下で実行するとどうなるか?

4
4
4

出力はこうなる。

挙動の解説

Go 1.21までは、for文のループでは同一のiを参照する。Print123関数の実行時にはforループが回り終わっているので、クロージャはi = 4を参照してしまうという話。 1 2 3を出力するために用いられるテクニックとして、以下の回避方法がある。

func Print123() {
    var prints []func()
    for i := 1; i <= 3; i++ {
        i := i // この行を追加
        prints = append(prints, func() { fmt.Println(i) })
    }
    for _, print := range prints {
        print()
    }
}

func main() {
    Print123()
}
1
2
3

i := iと書くと期待通りの出力が得られるが、「治ったからいいか」で済ませるのもスッキリしない。せっかくなので、この1行で解決する理由を言語化しておく。

変数のシャドーイング

for文の解説をする前に変数のシャドーイングについて触れておく。

import "fmt"

func main() {
    x := 10
    if x >= 10 {
        fmt.Println(x)
        x := 100
        fmt.Println(x)
    }
    fmt.Println(x)
}
10
100
10

コードを実行すると、このような実行結果が得られる。

if文のブロック内でブロック外のxと同名の変数xを宣言すると、宣言後に記述されているfmt.Printlnが参照するxはif文のブロック内で宣言されたxを参照される挙動に変化する。 その結果、2つめのfmt.Printlnではブロック内で宣言されたxに再代入された100が出力される。

ここでいう変数xのような変数は「シャドーイング変数」(内側のブロックで同じ名前をもつ変数)と呼ばれる。では、先ほどのPrint123関数がシャドーイング変数を利用することで予期した挙動に変わるのは何故か?

Print123の復習

シャドーイング変数を踏まえ、先ほどのコードを見直してみる。

func Print123() {
    var prints []func()
    for i := 1; i <= 3; i++ {
        i := i // クロージャから参照されるiはここに移った
        prints = append(prints, func() { fmt.Println(i) })
    }
    for _, print := range prints {
        print()
    }
}

forループも変数スコープなので、i := iをforループでシャドーイング変数として宣言すると、for文のループでは同一のiを参照していた挙動が変化し、参照先がシャドーイング変数として宣言されたiに移る。ループごとでiを保持するように変化したおかげで、無名関数のfmt.Println(i)の出力結果が1 2 3になる。

他のプログラミング言語ではどうか?

参考情報として、他のプログラミング言語で同様の実装がどう動くかを試す。今回はTypeScriptで試した。

function print123() {
     var prints: (() => void)[] = [];
     for (let i = 1; i <= 3; i++) {
         prints.push(() => {
             console.log(i);
         });
     }
 
     prints.forEach(print => print());
 }
 
 print123(); // 1 2 3 が出力される

ちなみに小ネタとして、for文のletをvarに書き換えると出力結果が変わる。

function print123() {
     var prints: (() => void)[] = [];
     for (var i = 1; i <= 3; i++) { // この行のletをvarに変更
         prints.push(() => {
             console.log(i);
         });
     }
 
     prints.forEach(print => print());
 }
 
 print123(); // 4 4 4が出力される

varで宣言した変数はprint123関数全体がスコープになるが、letで宣言した変数はforループそれぞれにスコープが限定される。スコープが限定された結果、console.logが1 2 3をそれぞれ参照できるようになり、期待通りの挙動を得られるという話。

とはいえ、TypeScriptを使用した開発環境でvarをあえて書きたい場面はないので、この罠を踏み抜く機会はないだろう。

Go1.22での挙動

ここでgolangでの挙動に話を戻す。

func Print123() {
    var prints []func()
    for i := 1; i <= 3; i++ {
        prints = append(prints, func() { fmt.Println(i) })
    }
    for _, print := range prints {
        print()
    }
}

func main() {
    Print123()
}
1
2
3

最初に記載したコードをGo1.22で動かすと、1 2 3が出力される。Go1.22におけるfor文の変化を公式のドキュメントで見てみる。

In a "for" statement, each iteration has its own set of iteration variables rather than sharing the same variables in each iteration.

https://tip.golang.org/ref/spec#Go_1.22 を見るとこのように書かれていて、日本語に直すと「for文では、各反復で同じ変数を共有せず、各反復で独自の反復変数セットを持つ」となる。つまり、forループの最初に暗黙的なi := iを追加する挙動となっていて、go1.21におけるの挙動とは根本的に異なることがわかる。

こうした小さいコードであれば、単純に「Go1.22使った方がいいじゃん!」となるが、この挙動の差によって既存の処理やテストが壊れる可能性がなくはないので、これからGo 1.22に移行を考えている方は要注意。自分が関わっている製品では大きな影響はなく、依存しているパッケージのアップデート待ち(Go1.22に対応待ち)に時間を取られていた記憶がある。

もし既存のテストが壊れることがあれば、影響のあるループをbisectを利用して特定可能。

参考: https://pkg.go.dev/golang.org/x/tools/cmd/bisect#hdr-Example

パフォーマンスはどうなのか?

func Print123() {
    var prints []func()
    for i := 1; i <= 1000000; i++ {
        // go1.21ではシャドーイング変数 i := i を記載
        prints = append(prints, func() { fmt.Println(i) })
    }
    for _, print := range prints {
        print()
    }
}

func main() {
    start := time.Now()
    Print123()

    since := time.Since(start)
    fmt.Printf("The for loop took: %s\n", since)
}

この環境にてループの回数を100万に増やし、処理時間はtime関数で計測する簡単な比較をしてみた。計測を数十回行ったところ、Go1.21でもGo1.22でも処理は3.12~3.18secの範囲に収まり、目立った有意差はみられなかった。

ちなみにproposalで例に挙がっているコードでもパフォーマンスの有意差は見られなかったらしい。ただ、各々が扱うアプリケーションに求められるパフォーマンス水準や複雑性はそれぞれなので、pprofでボトルネックの有無を計測しておくとより安心できそう。

まとめ

多くの人が踏み抜いた経験があろうfor文ループの挙動を変数スコープの観点で調べてみたメモでした。Go1.22では気にする必要はないものですが、「どういう挙動の変化があったのか」「変化する前の挙動はどういう仕組みだったのか」の観点で見返すと、興味深いトピックに巡り会える機会もあるようです。

参考リンク

休日の勉強に対して思うこと

「エンジニアは土日も勉強すべきか」という話題をSNSで見る機会があったので、思うところを書いた。

私の結論としては、個人の環境や立場において成果を出せているなら、その人が勉強するかしないかは自由だと思う。組織の一員として、期待される成果を達成しているならそれでいい。フリーランス個人事業主の場合、契約先の期待を満たすか、それを超える成果を提供していれば、その人の仕事に問題はないと考える。

勉強そのものはあくまで手段であり、目的ではないということを忘れてはいけない。学生時代、勉強時間の多さが成績や受験での評価に直結しないのと同様で、仕事においても、評価の基準は努力の量ではなく成果である。目標とする成果に至る過程で知識や技術の不足を感じたら、その際に勉強をするのが適切だと思う。

一方で、目的ではなく単純な興味がモチベーションとなる勉強にも意味があると考えている。別の形、思わぬ形で役立つことがあり得るからだ。たとえば、経営や営業に直接関わっていなくても関連書籍を読むことや、普段使用していないプログラミング言語での競技プログラミングへの挑戦してみるのはどうだろう。これにより、自分の専門領域以外の知識や思考法に触れ、新しいアイディアや技術の応用が生まれる可能性があるかもしれない。

「目的を達成するための勉強」「目的に結びつくか分からない、興味・関心による勉強」の両軸で勉強という行為を捉えて、各々の目的や興味・関心を大事にできればそれでいいと思う。

株式会社グラファーに入社しました

3月でコネヒト株式会社を退職し、4/1付で株式会社グラファーに入社しました。

グラファーに入った理由

大きな理由は2つあり、簡潔に言うと以下の2つです。

  • 行政に関わる仕事をやりたい。
  • Product Managerをやりたい。

グラファーの技術スタックのメインであるGoを仕事で書いてみたいという欲求も勿論あります。ですが、上記の大きな理由2つと比較すると、ついで程度の位置付けです。

行政に関わる仕事をやりたくなった経緯

「社会性のある仕事をする」というのが僕のポリシーで、このポリシー自体はこの4〜5年で全く変わっていません。3月まで仕事をさせて頂いていたコネヒト株式会社も、社会性を持ったプロダクトを作っていると強く思っています。

ただ、ここ最近の2年間で自分の気持ちが向く方向が変わる出来事がありました。それはコロナウイルスのワクチン接種を受けた時のことです。僕が初回の接種を受けた時期は2021年の夏頃で、時期的には東京23区の中でもかなり早い方でした。当時の僕は「早い時期に受けられてラッキーだな」と思う一方で、その状況を作ってくれた自治体に対しての感謝の気持ちを感じていました。そして、その気持ちは次第に「やってもらう側でいるだでいいのか?自分にも何かやれないか?」という恩返しに似た感情に変化していきます。

こういった心境の変化もあり、僕は行政の仕事に関心を惹かれるようになりました。その頃、4月からお世話になっているグラファーを知ったのは奇妙な偶然だったと今でも思います。ちなみにきっかけはAutifyの導入事例の記事でした。

Product Managerをやりたくなった経緯

「プロジェクトをマネジメントする」ロール(役割)はよく見かけますが、「プロダクトをマネジメントする」ロールは日本では当たり前の存在にはなっていないように思えます。それが当たり前になる前に、自分でも一度経験を積んでおきたいと思ったのが大きな理由です。

また、グラファーでのPdM(以下、PdM)を役割を聞いて新鮮に感じたことがありました。それはPdM自身もコードを書き、開発をおこなうところです。僕がこれまで見聞きしてきたPdMは営業やマーケティング領域出身の方が多く、本人は開発をしないスタイルが普通な印象を持っていました。この状態が単純に悪いとは思いませんが、PdM自身が開発側の事情を知らないことで、エンジニアとのコミュニケーションに壁が生まれるケースがあったと考えてはいます。

こういった背景から僕の中で「エンジニアがPdMをやったらどうなるのか?」という好奇心が生まれ、それに突き動かされて今が至ります。

まとめ

事業ドメインにも技術スタック(GoとかKubernetes)にも未経験のものが多いので、楽しみ半分、不安半分という心境で入社初日を終えました。まずはProduct Managerになるための下積みからスタートするので、半年後には使い物になるように頑張っていきます。

2021年のふりかえり

去年は結局ふりかえりをしなかったので、今年は面倒になる前に書いていく。総評として、悪くない一年だったと思う。

今年の仕事

  • TypeScript、React、Jestを使ったフロントエンド開発
  • CakePHPのバージョンアップ

CakePHP、TypeScript、React、Jestに明け暮れた1年でした。1月〜4月までは別口でGatsbyに触れる機会もあり、そこでGraphQLのキャッチアップもできた(せざるを得なかったとも言える)。また、空いた時間でGatsbyのバージョンアップにもチャレンジしたのは個人的な加点ポイント。

GW以降は社内で担当するプロジェクトが変わり、フロントエンドの実装を書く機会が急激に増えた。フロントエンドのテストに振れるのは数年ぶりだったので、Jest + testing-libraryの書き味に慣れるのが大変でした。

夏頃は組織で向き合う課題の1つである「CakePHPのバージョンアップ」に取り組み、結果的に2つのアプリケーションをCakePHP4系にアップグレードできました。普段一緒に仕事をしない同僚達とチームを組んで数ヶ月間を過ごし、非常にエキサイティングな時間でした。

仕事以外の活動

仕事以外の活動もそこそこ頑張ってました。

登壇

2020年末の登壇を含めると、定期的な登壇が出来た1年でした。CFPが全く通らない2019年頃と比べると大幅な進歩と言っても過言ではない筈。

2022年はGoをメインに勉強していくので、そちら方面での登壇を個人的な目標にしています。

輪読会

社内で輪読会が盛り上がる機運があったので、秋冬で輪読会を開催しました。読んだ本は以下の2冊です。

2冊目の輪読会は継続中で、来年の2月くらいに終わる想定で進めています。

ちなみに、開催にあたって2つのことを意識しました。

1つめは人数を少なめにして、上限を4〜5人に留めること。これは参加者の発言時間の確保とスケジュールの調整コスト軽減を目的としています。2つめは参加メンバーを同じ職種の人だけにしないこと。これは部署を跨いだコミュニケーションの機会を作りつつ、視点の偏りを防止するためです。

AWS認定

過去のブログでも振り返っていますが、秋にAWS認定を受験しました。Cloud PractitionerとDeveloper Associateの2つを受験し、無事に一発合格できました。

Cloud Practitionerは数日勉強すれば問題なかったんですが、Developer Associateはなかなかハードでした・・。Dynamo DBやKinesisをプロダクトを使う機会はこれまで無かったので、その辺のキャッチアップに手こずった印象です。スコアはギリギリで8割に届いてなかったので、見直しの詰めが甘かったら普通に落ちていたと思います。

試験の振り返りブログは以下。

来年の抱負

来年はGoやGraphQLを本格的に習得するために時間を使います。去年はPHPRDBMSAWS等の習熟に時間を使ったので、2022年は自分が習得していないものを学んでいく必要性を感じています。

あと、英語の勉強の再開ですね。秋にカンファレンス登壇やAWS認定と色々詰め込みすぎてしまい、勉強を2ヶ月以上中断してしまいました。これは自分のキャパシティを把握せずに張り切った結果なので・・キャパシティを俯瞰できる簡易的な仕組みを作って対処するようにしました。勉強自体は12月中旬から再開しているので、途切れないよう地道に積んでいくつもりです。

まとめ

2021年9月くらいまではひたすら引きこもって、たまに登壇。年の後半はAWS認定の勉強に打ちこんで燃え尽きて、英語の勉強を一度投げ出しました。11月、12月は二年近く会ってなかった友人達と食事や飲み会をして、そうしてるうちに年の瀬を迎えております。

反省点はいくつかあるものの、形になったアウトプットをいくつか残せたので及第点ではある筈。100点満点で75点くらいかな?

ということで、2022年は80点を目標に頑張っていきます。

リモートワークでの信頼関係に思いを馳せる

今の会社に入社したのが昨年の8月。物理出社した日は片手で数える程度で、ほぼ100%リモートワークで働いてきました。その過程でリモートワークについて自分なりに考えたものを言語化しておきます。

感情は信頼を揺さぶる

ここ数ヶ月の自分の思考を整理してみて思ったのは、「よく知らない相手は信頼できない」という結論でした。

リモートワークは必要な時に必要な事のみを話す働き方になりがちです。一緒に働いている同僚の好きな食べ物も知らないし、趣味が何なのかも知らない。そんな状態が続きます。仕事が上手くいっている時は問題ないかもしれませんが、互いの意見がぶつかった時はそうもいかず、僕は相手の意見を自分への攻撃と感じた経験が何度かあります。「よく知っている人なら、あそこまでネガティブな受け取り方はしなかったかも」と今でも思い返す機会があります。

そもそも、人間は感情に左右される社会的な生き物です。「酒や音楽の趣味が合う」「学生の頃に同じスポーツをしていた」程度の情報でも他者に親近感を感じ、興味を持つ人が大多数でしょう。そうした興味が相手に対する理解に繋がり、日常のコミュニケーションにも大きく作用します。

テキストベースに移行したコミュニケーション

リモートワークではチャットやドキュメントを介した文章上のコミュニケーションが主流となりました。IT企業に限らず、日常的なやり取りはチャットで実施されるのも珍しくはなくなりました。

ですが、テキストを表示している画面から感情は読み取れません。同じ空間にいないので、表情や雰囲気といった情報はありません。どう感じるかの判断はメッセージを受けとった人次第であり、ここの判断にも当人の感情が関わってきます。

「あなたの意見は〜の理由でよくないと思いました」というような言い回しも、受け取り方は相手次第です。僕の場合、信頼関係が既にある人からであれば謙虚な気持ちで耳を傾けようと思いますし、よく知らない人からであれば喧嘩を売られた気持ちになる可能性もあります。

自分の意見を論理的に言語化するのはもちろん大事ですが、意見を戦わせる機会がある人と友好的な関係を築くのも等しく大事なことです。

相手を知る努力

僕が普段意識していることが2つあり、1つめは「自分に関する話を積極的に話す」です。

現在所属しているコネヒト株式会社ではチャットツールにSlackを使用しており、分報チャンネルを使っているメンバーも一定数います。僕は自分の分報をTwitter感覚で使用しており、あらゆる内容をカジュアルに発言しています。

  • 12月になったからグラコロを食べたい。
  • 昼にラーメンを食べ過ぎて、今になって眠い。
  • ライブ・コンサートの抽選に当たった(もしくは落選した)。

このように仕事に直接関係の無い日常的な話をすることで、相手から「よく知らない人」と思われないようにしています。(※補足ですが、仕事に関係ある話はチームメンバーが集まっているチャンネルでするように意識しています)

分報のつぶやきをきっかけに他部署の人とコミュニケーションする機会が実際に生まれてもいます。

2つめは「自分がいるチーム以外にアンテナを張る」です。

会社でSlackを使っていると先程説明しましたが、社内には部署・チーム単位で様々なチャンネルが存在します。自分のチームに関係するチャンネルさえ見ておけば仕事に支障はほぼ出ませんが、僕は他チームのチャンネルも(無理のない範囲で)入るようにしています。チームごとの取り組みを知れますし、困り事にあえて首を突っこむことで普段関わる機会が少ない人との関係を作れたりもします。

どの程度の効果があるかは分かりませんが・・「よく知らない人」から「あちこちのチャンネルで見かける人」と周りから思われることで、自分に関わる精神的ハードルを下げられたら良いなと思っています。

まとめ

長々と書きましたが、「仕事以外の会話は無駄じゃない」というのが現時点での結論です。

僕がやっているプロダクト開発のように、変化が目まぐるしく、選択を頻繁に迫られる仕事は多く存在します。そこで戦い続けるためには、互いの意見を忌憚なく言い合えるチームが必要です。そのためには、物理的な出社が当然だった時代とは異なったやり方で人間同士の関係性を築く必要があるのだと考えています。

React Portalでmodalのレンダリング位置を制御する

Reactでmodalを実装する際に便利なものがあったのでブログに纏めてみます。ちなみに、React 16時代から利用できる機能なので目新しい話題ではないことを冒頭で断っておきます。

参考 : React v16の機能紹介

React Portalについて

React Portal は簡単に言うと「子コンポーネントレンダリングするDOMノードを指定する」仕組みです。Reactでは大元となるDOMに対してコンポーネントの親子関係を構築していくのが基本ですが、DOMの階層に外にいるDOMノードにコンポーネントレンダリングしたいケースも起こりえます。その際に利用するのがReact Portalです。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="robots" content="noindex">
  </head>
  <body>
    <!-- 大元となるDOM -->
    <div id="react-root"></div>
    <!-- React Portalを使用したレンダリング先となるDOM -->
    <div id="portal-root"></div>
  </body>
</html>

言葉では伝わりづらいので、実際のHTMLも載せてみます。主要なコンポーネントレンダリングされるのはid「react-root」のdivですが、特定のコンポーネントをid「portal-root」のdivに対してのレンダリングが可能になるのです。

modal実装時の悩みどころ

要素に「重なり順」という関係性が生まれ、子要素が親要素の下に表示されるのはHTMLの基本です。ですが、modalは親を飛び越えていく必要があります。

modalの表示に支障が出ているHTMLのサンプルを実際に作ってみました。

https://codepen.io/rukiadia/pen/gOxBvNz

modalがサイドメニューの下に潜り込んでしまう様子
modalがサイドメニューの下に潜り込んでしまう様子

私が作成したサンプルでは、modalが画面左のサイドメニューの下に潜り込んでいます。modal要素は画面全体に広がる挙動を想定されるので、この見た目は違和感を感じざるをえません。要素の重なり順をz-indexで複雑に制御しているアプリケーションでは、このような状況が容易に起こりえます。

【余談】 z-indexについて理解を深めたい方は「z-index」「スタッキングコンテキスト」をキーワードに参考記事を探してみるのがオススメです。

React Portalを使ってみる

import React from 'react'
import { createPortal } from 'react-dom'

const modalRoot = document.createElement('div')
modalRoot.setAttribute('id', 'portal-root')
document.body.appendChild(modalRoot)

export const Modal: React.FC<Props> = ({ children, closeModal, isOpen }) => {
  return createPortal(
    isOpen ? (
      <Overlay onClick={closeModal}>
        <Container onClick={(event) => event.stopPropagation()}>
          {children}
        </Container>
      </Overlay>
    ) : null,
    modalRoot
  )
}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="robots" content="noindex">
  </head>
  <body>
    <!-- 大元となるDOM -->
    <div id="react-root"></div>
    <!-- レンダリング先のDOMをModalコンポーネントが動的に生成 -->
    <!-- <div id="portal-root"></div> -->
  </body>
</html>

「親コンポーネントから渡されるchildrenをmodal内に表示」「modalのレンダリング先となるDOMを生成」の2つの役割を持つコンポーネントのコード例です。(必要最低限のコードにするため、CSSの記述などは意図的に省いてます)

レンダリング先のDOMをあらかじめ用意することも可能ですが、modalに関する事情をコンポーネント内に纏めたかったのでこうしています。また、テストコードが書きやすくなる利点もあるので、動的に生成する手法がオススメです。

参考 : Testing LibraryのModalテストコード例

まとめ

以上がReact Portalの仕様例の説明になります。

react-modal のようなライブラリを使用すれば必要のない苦労にはなりますが、自作したいケースもあるかもしれません。その場合はReact Portalを使用することで「画面の表示」と「コンポーネント構造」を切り離して実装を進めていくと、開発が楽になる筈です。