「Go言語らしさ」とは何か? Simplicityの哲学を理解し、Go Wayに沿った開発を進めることの良さ
プログラミング言語には作者の設計思想が強く現れますが、Go言語もまた後発であることを生かし、しっかりとした思想に基づいて開発されています。公式のドキュメント等で語られているSimplicityの哲学を理解し、Go言語らしさ、Go Wayに従って開発する良さについて、はてなチーフエンジニアの松木雅幸(Songmu)さんが解説します。
id:Songmuと申します。はてな東京オフィスでチーフエンジニアを務め、Mackerelというサーバー監視のSaaS開発においてプロダクトマネージャーを担当しています。
筆者が勤めている株式会社はてなでは、2013年よりGo言語を開発に用いるようになりました。導入当初はMackerelで用いられる監視エージェントであるmackerel-agentから始まり、現在ではURL外形監視のためのサブシステムや、時系列データベースのミドルウェア、最近では新規プロジェクトでの採用など利用シーンが多岐にわたっています。
筆者はJavaの経験は少しありましたが、Perlなどの動的型付けのスクリプト言語での開発が長く、最初はGoに慣れない点もありました。しかし、今では大好きな言語のひとつになり、筆者のGitHubでは、Goのツールやライブラリを数多く公開しています。
今回、「Go言語らしさ」をテーマに記事を執筆してほしいという連絡が来たときは、「これは大変な依頼が来てしまったな」と思いました。そのテーマで記事を書くにはよりふさわしい方がいるようにも思いましたし、Goは後発のプログラミング言語でもある分、設計思想がしっかりしており、それらの多くは既に作者や開発者チームによって語られているからです。
プログラミング言語はその作者がしばしば「優しい終身の独裁者」などと称されることがあるように、特に作者の設計思想が強く現れるソフトウェアです。Goの場合でも、初期開発者のRobert Griesemer、Rob Pike、Ken Thompsonの3名や、コア開発者のRuss Coxにより、Go Blogや彼らのプレゼン資料、ブログなどでその背景や設計思想が綴られています。
本稿では、Go言語の作られた背景・設計思想に改めてふれるとともに、筆者が考える「Go言語らしさ」や、Goらしいコードを書くために心掛けていることをお伝えします。
Go言語らしさとは
Go言語が業界に急速に受け入れられている大きな理由に、「そもそもの思想の筋の良さ」と「現代のニーズにマッチした言語機能」があると筆者は考えます。
つまり、過去に正しく学び、現代の開発に必要な機能を盛り込み、加えてこれからの未来に求められるフィーチャーを提案し実現しようとしているという、過去・現在・未来がバランスよく考慮された優れた設計になっているのです。
言語設計の経緯や、現在の潮流の中で言語に求められている機能を把握すると、一見特殊にも思えるGoの言語機能がなぜそのようになっているのか、どういった言語特性が世の中に受け入れられているのかを理解することができ、より「Goらしい」コードを書けるようになるでしょう。
Goが作られた背景
Goが作られた背景としては、Rob Pikeによるドキュメント「Go at Google」を読むと、Google社内における大規模ソフトウェア開発の生産性やスケーラビリティのためだということが分かります。
The goals of the Go project were to eliminate the slowness and clumsiness of software development at Google, and thereby to make the process more productive and scalable. The language was designed by and for people who write―and read and debug and maintain―large software systems.
Go's purpose is therefore not to do research into programming language design; it is to improve the working environment for its designers and their coworkers. Go is more about software engineering than programming language research. Or to rephrase, it is about language design in the service of software engineering.
筆者が特にGoに対して好ましく思っている点は、ここに書かれているように、Goがプログラミング言語研究のための言語ではなく、実際の問題解決のための言語だということです。その点は、プログラミング言語そのものが好きな人にとって物足りなく感じられる部分ではあるのでしょう。
また、上記の「Go at Google」のほかにも、公式のFAQには開発のコンセプトや思想が事細かに書かれています。それぞれの記事では、Goが作られた背景には以下のような課題があることが述べられています。
- プログラミング言語そのものが開発されている環境と、実際に使われている環境が異なる
- ソフトウェアを開発する速度にもスケーラビリティが求められている
- 既存の静的言語の多くはパッケージの依存管理が大変である
- GCと並列計算の両方が、一般的な言語ではうまくサポートされていない
- マルチコアネイティブな言語が求められている
また、これらを踏まえ、FAQの中の「何故新しい言語を作っているのか」という項目では以下のように述べられています。
Go is an attempt to combine the ease of programming of an interpreted, dynamically typed language with the efficiency and safety of a statically typed, compiled language. It also aims to be modern, with support for networked and multicore computing.
Goは、動的型付けのスクリプト言語における開発の効率性と容易さと、静的型付けの言語の安全性を融合させる試みであり、ネットワークやマルチコアネイティブなモダンな言語であることを目標にしていることが分かります。
実際に筆者は、Perlのようなスクリプト言語での開発経験が長いのですが、Goにはすぐ馴染(なじ)むことができました。スクリプト言語の感覚で開発できる静的型付け言語であるように感じています。
例えば、スクリプト言語での開発の場合、少しずつ開発コードとテストコードを交互に書いて動作を細かく確認しながらインクリメンタルに開発を行いますが、Goでもそのリズムを大きく変えることなく開発が可能です。Goのコンパイルが速くテストも書きやすい点が大きく寄与していると感じます。
Goを作った人たち
Goは、Robert Griesemer、Rob Pike、Ken Thompsonの3名によって開発が始められました。
Rob PikeとKen Thompsonの2人は、計算機の世界では神様と言っていい存在です。Ken Thompsonは、C言語やUNIX、UTF-8のオリジナル開発者の一人です。Rob PikeもUNIX開発に携わり、UTF-8の初期設計を行ったことで知られています。
そんな彼らが設計した言語ですから、過去の言語設計で失敗した反省が、Goには活かされているようにも感じます。例えば、以下のような点が挙げられるでしょう。
- 変数の型宣言が後置である
- switch文で、デフォルト挙動でbreakする
- 外部パッケージの読み込みが、マクロではなく構文として用意されている
- 暗黙の数値変換を提供していない
- 関数の返り値を複数持つことができる
ScaleとSimplicity
Goのゴールは、2種類のScale ―システムのスケーラビリティと開発のスケーラビリティ― であると、Russ Coxは「Toward Go 2」という記事で述べています。
この2つを実現する上で大事なキーワードが、Simplicityです。Goの言語としての優位性は、シンプルなものを組み合わせたバランスの良さにあります。
多くの言語が同じような機能追加競争を行うと、ともすればそれぞれが似通った言語になりかねません。一方、Goは機能を削ることで勝負しています。コア開発者の全員が必要だと思った機能しか言語に取り込まないという方針が、言語の仕様をミニマムに保つ上で非常にワークしていると言えます。
キーワードが25個しかないミニマルな文法や、過剰な表現力を持たせず、素朴さを是とする文化も、こうした思想が反映されているのではないでしょうか。
また、書き手にとっての単純さのために、Go言語が内部で複雑さを隠蔽してくれている機能もあります。それが、GCであり、goroutineとchannelといった並行・並列処理のための機能、そしてインターフェース機能です。
単純ではあるものの強力で奥深いこれらの機能を組み合わせることが、Goプログラミングの醍醐味(だいごみ)といえるでしょう。
Rob Pikeの「Simplicity is Complicated」という資料も参考にしてください。
Goらしいコードを書くことを心掛ける
ここまで、言語を開発した背景や、言語が目指すゴールから「Goらしさ」を概観してきました。ここからは、実際にGoを書く上で、筆者自身が心掛けていることを述べていきたいと思います。
Goに入ってはGo Wayに従う
Goを書く上で大事なことは、まずはGoの作法に素直に従うことです。ほかの言語から来ると、Goのお作法は一見奇妙に感じることもありますが、その違和感をぐっと飲み込んで、素直にGoのやり方に沿ってしばらくコードを書いてみましょう。
自分が慣れている言語のやり方を中途半端に取り込まずに、全面的に従ってみましょう。そうすることで、最初は奇妙に見えた作法が徐々に腑(ふ)に落ちるようになり、Goらしいコードが身に付いてきます。
例えば、簡潔なパッケージ名と短い変数名は、それでも問題ないようにスコープやパッケージの役割を小さく保つためのプラクティスですし、例外のないエラーハンドリングに関しては大域ジャンプや意図せぬクラッシュを排することで特に、並列、並行処理時の見通しを良くするために有用であることが理解できます。
Goらしいコードを書くためのツール群
Goらしいコードを書くためにはどうすればよいのでしょうか。まずは、公式で提供されているガイドライン「Effective Go」に目を通すと良いのですが、いきなり通読するのは少し大変です。
実は、これらの一部の作法を強制し守らせるためのツール、いわば矯正ギプスのようなツール群が、標準や公式で提供されています。具体的には、gofmt
やgoimports
といったコードフォーマッター、そしてgo vet
やgolint
といったlinterです。
こういったツールを適切に使っていれば、基本的なGoらしいコードスタイルが自然と身に付いてきます。
Goの作法を遵守しておけば、Goを用いたチーム開発で基本的なコーディング規約をいちいち策定する必要がありませんし、スタイルの好き嫌いに起因する「自転車置き場の議論」を避けることもできます。これは開発をスケールさせる上で、地味に重要なポイントです。
コードフォーマッター ── gofmtとgoimports
gofmt
は、Go言語のパブリックリリース当初から付属しており、現在も標準バンドルされているコードフォーマッターです。
gofmt
には、コーディングスタイルによってフォーマッターをカスタマイズするような機能が、あえてありません。例えば、gofmt
のインデントはハードタブ(タブ文字)を用いますが、これをソフトタブにカスタマイズする機能はありません。
こうしたカスタマイズの制限は、適切な決め打ちの単一なフォーマットルールを、あらゆるプロジェクトで統一的に適用させることへの言語開発者側の想いが感じられます。筆者もインデントがハードタブであることに最初は戸惑いましたが、そのうち慣れました。
goimports
は、公式で提供しているgofmt
の上位互換ツールです。コードのフォーマットに加えて、importの自動解決(パッケージ名のアルファベット順にソート、不要なパッケージの削除など)の機能を備えています。
go get
で、以下のようにインストールする必要があります。
% go get golang.org/x/tools/cmd/goimports
goimports
では、importの解決の関係で実行にやや時間がかかることや、gofmt
にある-s
、-r
オプションがなかったりと、両者には細かい違いがあります。ユースケースによって使い分けが必要ですが、筆者はほぼgoimports
のみを利用しています。
コードフォーマッターは、リポジトリにコードをコミットする前に実行するのが一般的ですが、IDEやエディタの設定で、コードを保存したときに自動実行させるようにしている人も多いようです。
linter ── go vetとgolint
標準でlinterが提供されていることも、Goの素晴らしい点です。
linterは、コードの静的解析を行い、コード上の怪しい箇所を機械的に指摘してくれるものですが、Goが提供している各種のlinterは、スタイルが揃っていない箇所や、Goらしくない書き方に加え、コード上でバグが発生しそうな箇所も指摘してくれる優れものです。
標準でバンドルされているgo vet
のほか、公式ツールのgolint
があります。go vet
はバグの原因になりそうなコードを検出するツールで、golint
はそれに加えて、Goらしくないコーディングスタイルを検出して警告してくれます。
golint
は、go get
で以下のようにインストールする必要があります。
% go get github.com/golang/lint/golint
もし手元にGoで書いたコードがあり、これまでこれらのlinterを適用したことがなければ、以下のコマンドを実行してみてください。
% go vet ./...
% go list ./... | xargs golint -set_exit_status
もしかすると、うんざりするような量の警告が出力されたかもしれません。変数の命名規則や宣言位置、コメントのフォーマット、分岐の書き方など、小うるさく感じられる内容の警告も多いでしょう。
しかし、細かい警告にきちんと対応することで、見通しの良い、Goらしいコードに近づきますから、指摘をちゃんと直しておくことが後々のためになります。慣れてくれば、ほとんど警告が出ないようにコードを書くことができるようになるでしょう。
うんざりし続けないためにも、プロジェクト当初からこれらのコマンドを定期的に実行し、忘れないように、CIの中に組み込むと良いでしょう。
また、golint
には-min_confidence
というチェックの厳しさを指定するオプションがあります。デフォルトが0.8で、数値が小さくなるほど厳しくなります。デフォルトの設定に慣れてきたら、もっと小さな値に設定しても良いでしょう。
これらのフォーマッターやlinterを活用しながらGo言語で開発を行い、慣れてきたら「Effective Go」に立ち戻って読み返すことで、改めてGoらしさを理解できるようになるでしょう。
ツールやエコシステムまでを含むGoの哲学
少し脱線しますが、Goは公式でほかにも多くのツールを提供しています。ドキュメント閲覧ツールのgodoc
、変数や関数をリネームするgorename
、ソースコードの静的解析ツールguru
、プロファイラツールpprof
などです。
これらのツールは、Goで開発するに当たって非常に有用です。特にエディタと連携して、補完やリファクタリング機能などの統合開発環境を提供する上で力を発揮します。Goは大きなIDEを提供するのではなく、エディタ内でツールを組み合わせることで、各種エディタでIDEのような操作性を実現できるようになっています。
これは非常にGoらしい発想だと思います。小さくてシンプルなものを組み合わせて高機能な仕組みを実現することは、UNIXの考え方にも通じるGoの哲学です。
このように、Goは言語そのものだけではなく、ツールやエコシステム含めた言語環境込みで設計されているところが強みと言えます。
Goらしいテストの書き方
現代のソフトウェア開発においてテストの重要性は増しており、もはやテストコードを書くことは当たり前となりました。Goでもモダン言語らしく、標準でtesting
というテストパッケージが提供されています。
Goのテストのやり方は最初は少し独特ですが、覚えることが少なくて実は簡単です。最低限、以下のことだけを覚えておけばいいでしょう。
-
go test
で実行する -
*_test.go
のように命名されたファイルがテストファイルとなる -
TestXXX(t *testing.T)
のようにTest
で始まり、t *testing.T
を受け取る関数がテストケースになる -
t.Error()
やt.Errorf()
などが呼び出されるとテスト失敗となる
明示的に失敗させることはできて、明示的に成功させることができない点が少し分かりづらい点かもしれませんが、そこは慣れの問題です。
テストコードの例とTable Driven Test
典型的なテストコードは以下のようになります。
import testing func TestHello(t *testing.T) { expect := "Hello" got := Hello() if got != expect { t.Errorf("got: %s, expect: %s", got, expect) } }
このコードでは、Hello()
という関数の出力が正しく "Hello" となっているかをテストしています。
非常に単純明快なテストコードですが、assertやmockの仕組みが標準で提供されていないことに対して不満を感じることもあるかもしれません。
慣れないうちは、必要に応じてtestifyなどのサードパーティ製のライブラリを使っても良いかもしれません。ただし、筆者がそれらの必要性を感じることは減ってきました。
例えば、Table Driven Testの手法を使えば、assertを書きまくる必要もなくなり、単純なエラーメッセージで十分になります。
Table Driven Testは、標準パッケージでもいたる所で使われているテクニックです。具体的には、テスト対象の関数に対する入力と期待される出力のペアを、必要なテストケース分だけテーブルに記述し、それに対して網羅的にテストを実行する手法です。一度テーブルを組んでしまえば、テストケースの追加が簡単になります。
また、Table Driven Testでテストが書けるということは、対象関数の入力と出力が明確で、設計が綺麗な証拠でもあります。ほとんどのテストは、可能な限りTable Driven Testに落とし込むと良いでしょう。
mockに関しても、インターフェース機能をうまく使えば実現することができますし、うまくインターフェースに落とし込むことは、コード設計上も有用だと言えます。
標準パッケージから「Goらしい手法」を学ぼう
Goでは、テストに限らず、標準パッケージで多くのことがまかなえるため、サードパーティ製のライブラリに頼る必要が、あまりありません。
ただ、そのやり方が、これまでの他言語でのマインドセットと異なるため、解法を見つけられず、慣れたやり方のライブラリを探してしまいがちです。
実際には「Goらしい手法」が用意されていることが多く、それを覚えれば解決できることも多いのです。そういった手法は、標準パッケージのソースコードのいたる所に散りばめられています。参考になるので読んでみると良いでしょう。
なお、説明は割愛しますが、testing
パッケージには、ベンチマークの仕組みも含まれています。これはパフォーマンス測定上有用で、これが言語標準で簡単に行える点も嬉しいポイントです。
Goらしいコードを書く上で心掛けているさらにいくつかのこと
さらに、実用的なGoらしいコードを書く上で筆者が心掛けていることや考えていることを、いくつか取り上げて解説します。
panicを使わない
Go言語開発者にとってはもはや当たり前ですが、Goに例外はないと考えたほうが良いでしょう。
panic
という組み込みの関数はありますが、これは例外というよりも、本当に致命的な問題で終了するときに使われるもので、基本的には使いません。
その代わり、関数から値を返す際に、その末尾に組み込みのerror
型を返し、それをnil
と比較してエラー処理を行うことが基本の形となっています。
result, err := doSomething() if err != nil { // エラー処理 }
if err != nil {
の頻出は、他言語開発者がGoのアレルギーを起こしてしまいやすいポイントでもあります。
ただ、これもまた慣れの問題だと思うようになりました。このコードの流れの方が、エラーチェックを行っていることが明らかで、例外のキャッチ漏れなどを気にする必要がないという利点があるように感じています。
正規表現を避けてstringsパッケージを活用する
強力な正規表現機能を備えたスクリプト言語で開発した経験があると、文字列操作はなんでも正規表現でやりたくなってしまいます。
Goの標準の正規表現パッケージであるregexp
は十分に高機能ではあり、現実的な速度で動きはするものの、それほどパフォーマンスは高くありません。実際、スクリプト言語の正規表現ライブラリと同等程度の速度しか出すことができません。これはコンパイル言語にとってはかなり大きなパフォーマンスイシューと言えるでしょう。
幸い、Goの文字列操作パッケージであるstrings
パッケージが非常に充実しているため、正規表現の代わりにその中の関数のいずれかが使えないか調べてみると良いでしょう。
どうしても正規表現が使いたい場合
もちろん正規表現を使いたい場合もあります。その場合、正規表現オブジェクトの生成にはコストがかかるため、実行時に動的に生成するのは可能な限り避け、初期化時に生成しましょう。
具体的には、パッケージのトップレベルか、init()
の中でオブジェクトの生成を済ませてしまいます。
var digitsReg = regexp.MustCompile(`\d+`)
ここでは文字列をバッククオートで囲んだraw string literalを用いています。余計なエスケープを避けるため、正規表現の指定時には常時利用すると良いでしょう。
ちなみに、MustXXX
という命名の関数は、引数が必ず正しいことが期待されており、誤った引数を指定した場合にpanic
が起こることを示す命名規則です。つまり、MustCompile
に誤った正規表現文字列を渡すとpanic
します。これは、実行中の関数の中で利用してはいけません。
実行中に正規表現を動的に組み立てたい場合には、代わりにregexp.Compile
を以下のように使います。
reg, err := regexp.Compile(`...` + hoge) if err != nil { // エラー処理 }
この場合、不正な正規表現文字列が渡された場合にはエラーが返却されるので、プログラムをクラッシュさせず、適切にエラー処理を行うことができます。
ただ、この節の冒頭で述べたように、動的に正規表現文字列を組み立てて実行中に正規表現オブジェクトを生成するのは、パフォーマンス上のオーバーヘッドも大きいため、あまりやらない方が良いでしょう。
インターフェース機能を活用する ── io.Reader の場合
Goのインターフェース機能の中で、その強力さが最初に分かりやすく実感できるのはio.Reader
ではないでしょうか。
何らかの大きめの入力を受け取る関数を定義するとき、サイズの大きな文字列をstring
で渡そうとしたり、開いたファイルを*os.File
のまま渡すようなシグネチャを定義してしまいたくなりますが、これはよくありません。
Goの標準io
パッケージには、入力ストリームを抽象化してくれるio.Reader
というインターフェースがあり、そちらを活用するのが綺麗です。例えば、以下のような形です。
func main() { f, err := os.Open("/path/to/file") if err != nil { log.Println(err) os.Exit(1) } defer f.Close() doSomething(f) } func doSomething(r io.Reader) error { // 入力に対する処理 }
ここでos.Open
が返している変数f
の型は*os.File
ですが、doSomething
のシグネチャでは、io.Reader
を受け取るように定義しているところがミソです。
このようにすることで、汎用的で効率もよく、依存も切り離されて、テストもしやすいコードを書くことが可能になります。
継承よりコンポジション
「継承よりコンポジション」とは、オブジェクト指向に関する警句として近年よく聞くようになった言葉です。
Goでは、structを定義して、それに対してメソッドを定義するオブジェクト指向のようなことが行えますが、一般的なオブジェクト指向言語における継承を用いた階層構造はサポートしていません。その代わり、埋め込みによって委譲のようなことを実現することができます。
Goにおいては、1つのstructに多くのフィールドやメソッドを設けて大きなクラスを作ろうとするのではなく、特定の型を埋め込んで委譲したり、小さい汎用的なインターフェースを満たすメソッドを実装したりすることで、コンポジションを実現するようにクラス設計の思考を切り替えることが肝要です。
これに慣れてくると、Goらしい自然なクラス設計ができるようになるでしょう。
例えば、標準のio
パッケージには、io.ReadWriter
というインターフェースがありますが、この中身は、以下のようにio.Reader
とio.Writer
がコンポジションされた定義となっています。
type ReadWriter interface{ Reader Writer }
継承を敢えてサポートしない割り切りも、Goらしくて好ましいと筆者は思います。
SOLIDなデザイン
オブジェクト指向における「SOLID」という原則があります。オブジェクト指向の原則の中でも、特に重要な5原則からその頭文字が取られています。
- S : The Single Responsibility Principle(単一責任の原則)
- O : The Open Closed Principle(オープン・クローズドの原則)
- L : The Liskov Substitution Principle(リスコフの置換原則)
- I : The Interface Segregation Principle(インターフェース分離の原則)
- D : The Dependency Inversion Principle(依存性逆転の原則)
これは、継続的に開発されるソフトウェアがスパゲッティコードにならないようにする重要な原則です。これらの原則を適用する際に、GoのSimplicityの哲学や、一つ一つの機能、特にインターフェース機能は、驚くほどの効果を発揮します。
継承などの機能がないが故に「Goはオブジェクト指向言語ではない」と言われることもあります。しかし、それは不必要な機能が削られているだけの話で、Goには現代のソフトウェア設計において必要な機能はきっちりと詰め込まれており、オブジェクト指向的な設計も十分にできるのです。
GoとSOLID原則に関しては、Dave Cheneyの「Solid Go Design」という記事に詳しく書かれてるので、併せてお読みださい。
並行・並列処理
本稿ではあまり取り上げられませんでしたが、やはり、goroutineとchannelを用いた並行・並列処理こそがGoの真骨頂であり、Goらしさの神髄と言えます。
筆者がGoを書いて一番衝撃だったのは、同期処理と非同期処理を混在させて書けることです。PerlのAnyEventやNode.jsのような非同期前提のフレームワークや言語を用いると、多くの処理を非同期で扱う必要があるため、コードが複雑になってしまうことがあります。
しかし、Go言語では、大筋では同期的に処理を記述して局所的に非同期処理にすることが、goroutineを使えば驚くほど簡単に実現できるのです。
並行・並列処理には、用途に応じたさまざまな実装パターンがありますが、Go言語では、goroutineとchannelというたった2つのシンプルかつ強力な機能で実現できます。これも、Goらしいポイントです。
ここで語り尽くすことはできませんが、是非、goroutineとchannelを使いこなして、奥深いGoの並行・並列処理を堪能してください。
あまりにも簡単に並行・並列処理が書けてしまうため、goroutineを不必要に使いたくなってしまうこともありますが、逆に非効率になったり、コードの見通しが悪くなってしまったりするため、必要なところで使うにとどめ、使い過ぎには注意しましょう。
まとめ
Goらしさや、Goらしくコードを書く方法について、筆者なりに書かせてもらいました。Goの魅力を語り尽くすことはできませんが、筆者がGoを書いていて気持ちが良い所を、最後にまとめて書き出しておきます。
- 現実的に速いため、パフォーマンスをそれほど気にしなくて良い
- テストと実装のリズムが、スクリプト言語のように軽快に書ける
- 明示的に記述させる言語仕様や原則が、逆に迷いを排してくれる
- 周辺ツールチェインが整っている
- 同期処理と非同期処理を混在させて書ける
- コードの公開のしやすさ
- 後方互換に配慮されている安心感
Goは何より実用言語であり、生産性が高い点が魅力です。そのSimplicityの哲学を理解し、Goの作法に素直に従って開発を進めてみると、Goの良さやその生産性の高さと効率の良さがより分かってくるでしょう。是非、Goを使ってみてください。
執筆者プロフィール
松木 雅幸(まつき・まさゆき)songmu
The Go gopher was designed by Renee French. gopher.ai was created by Takuya Ueda. Licensed under the Creative Commons 3.0 Attributions license.
編集:薄井千春(ZINE)