シェアしました。
新しい言語に移行するのは常に大きな決断です。その言語をよく知る人がチーム
メンバーに1人しかいない時などは特にそうです。今年の初め、我々は Stream
の主要言語を Python から Go に切り替えました。この記事では、なぜ私達が
Python から Go に移行しようと決断したのか、その理由を説明します。
メンバーに1人しかいない時などは特にそうです。今年の初め、我々は Stream
の主要言語を Python から Go に切り替えました。この記事では、なぜ私達が
Python から Go に移行しようと決断したのか、その理由を説明します。
Go を使う理由
理由1 - パフォーマンス
Go は速いです! Go は極端に速い。そのパフォーマンスは Java もしくは C++ に
匹敵します。
私達のユースケースでは、Go は Python より30倍速いです。Go と Java を比較
したベンチマークはこちらです。
匹敵します。
私達のユースケースでは、Go は Python より30倍速いです。Go と Java を比較
したベンチマークはこちらです。
理由2 - 言語パフォーマンスの問題
多くのアプリケーションにとって、プログラミング言語は、単にアプリとデータ
ベースを繋ぐものにすぎません。言語そのもののパフォーマンスは通常あまり
重要ではありません。
ベースを繋ぐものにすぎません。言語そのもののパフォーマンスは通常あまり
重要ではありません。
しかしながら Stream は、500の会社と200万人のエンドユーザ向けにニュース
フィードのインフラを提供する API プロバイダーです。私達は Cassandra、
PostgreSQL、Redis 等を長年最適化していますが、だんだん使用している言語
の限界に近づいています。
フィードのインフラを提供する API プロバイダーです。私達は Cassandra、
PostgreSQL、Redis 等を長年最適化していますが、だんだん使用している言語
の限界に近づいています。
Python は素晴らしい言語ですが、serialization/deserialization、ranking そして
aggregation のような用途に関して、パフォーマンスがかなり悪いです。
私達は、Cassandra がデータ取得に1ミリ秒かかり、 Python がそれを
オブジェクトに変換するのにさらに10ミリ秒費やす、
というようなパフォーマンス問題にしばしば出くわしました。
aggregation のような用途に関して、パフォーマンスがかなり悪いです。
私達は、Cassandra がデータ取得に1ミリ秒かかり、 Python がそれを
オブジェクトに変換するのにさらに10ミリ秒費やす、
というようなパフォーマンス問題にしばしば出くわしました。
理由3 - 開発者の生産性が高く、いい意味で創造的でないこと
package main
type openWeatherMap struct{}
func (w openWeatherMap) temperature(city string) (float64, error) {
resp, err := http.Get("http://api.openweathermap.org/data/2.5/weather?APPID=YOUR_API_KEY&q=";; + city)
if err != nil {
return 0, err
}
defer resp.Body.Close()
var d struct {
Main struct {
Kelvin float64 `json:"temp"`
} `json:"main"`
}
if err := json.NewDecoder(resp.Body).Decode(&d); err != nil {
return 0, err
}
log.Printf("openWeatherMap: %s: %.2f", city, d.Main.Kelvin)
return d.Main.Kelvin, nil
}
あなたが Go の初心者なら、このコードを読んだ時に大して驚くことはない
でしょう。
この例では、複数の戻り値を変数に格納すること、データ構造、ポインタ、
フォーマット、そして組み込みの HTTP ライブラリを示しています。
でしょう。
この例では、複数の戻り値を変数に格納すること、データ構造、ポインタ、
フォーマット、そして組み込みの HTTP ライブラリを示しています。
最初にプログラミングを始めたとき、私は Python のより進んだ機能を使うのが
好きでした。Python ではとても創造的にコードを書くことができました。
例えば、こんな感じです。
好きでした。Python ではとても創造的にコードを書くことができました。
例えば、こんな感じです。
- MetaClass を使って初期化時にクラスを self に登録する
- True、False の入れ替え
- 組込み関数のリストに関数を追加
- 特殊関数経由で operators をオーバーロード
- @property デコレータを使って関数をプロパティとして使う
これらの機能は使うのが楽しい反面、多くのプログラマーが同意するように、
他人の書いたコードを理解するのがより難しくなります。
他人の書いたコードを理解するのがより難しくなります。
Go はあなたを基本に忠実にしてくれます。他人のコードがとても読みやすく
なり、書いてあることがすぐに理解できます。
なり、書いてあることがすぐに理解できます。
注: どのくらい「簡単」かは、もちろんあなたのユースケースに依存します。
もしあなたが基本的な CRUD API を作成したかったら、私は Django +
DRF もしくは Rails をお勧めします。
もしあなたが基本的な CRUD API を作成したかったら、私は Django +
DRF もしくは Rails をお勧めします。
理由4 - 並列処理とチャンネル
言語として、Go は物事をシンプルに保とうと試みています。新しい
コンセプトは導入していません。
焦点を置いているのは、信じられないほど速く、作業がしやすいような
シンプルな言語を作ることです。革新的な唯一の分野は goroutine と
channel です。(1977年にスタートした並列システムの理論 CSP の
コンセプトを100%正しくしておくために、古いアイデアへのより多くの
新たなアプローチがあります。)
goroutine は Go の軽量なスレッド、そして channel は、goroutine 間で
通信するための方法です。
コンセプトは導入していません。
焦点を置いているのは、信じられないほど速く、作業がしやすいような
シンプルな言語を作ることです。革新的な唯一の分野は goroutine と
channel です。(1977年にスタートした並列システムの理論 CSP の
コンセプトを100%正しくしておくために、古いアイデアへのより多くの
新たなアプローチがあります。)
goroutine は Go の軽量なスレッド、そして channel は、goroutine 間で
通信するための方法です。
goroutine は作るのがとても簡単で、メモリを数キロバイトしか消費
しません。また、goroutine はとても軽いので、同時に何百もしくは何千も
動かすことが可能です。
しません。また、goroutine はとても軽いので、同時に何百もしくは何千も
動かすことが可能です。
また、channel を使うと goroutine 間の通信が可能になります。Go
ランタイムが並列処理に必要な複雑な処理を代わりに担ってくれます。
goroutine と channel を用いた並列処理は、複雑な開発を除いて、
CPU のリソースを効率的に利用し、並列の IO を扱うことを大変簡単に
します。
Python や Java と比べて、goroutine は最小限のコードしか必要と
しません。
関数呼出し時に "go" というキーワードを関数に付け加えるだけです。
ランタイムが並列処理に必要な複雑な処理を代わりに担ってくれます。
goroutine と channel を用いた並列処理は、複雑な開発を除いて、
CPU のリソースを効率的に利用し、並列の IO を扱うことを大変簡単に
します。
Python や Java と比べて、goroutine は最小限のコードしか必要と
しません。
関数呼出し時に "go" というキーワードを関数に付け加えるだけです。
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
Go の並列処理はとても簡単です。Node
が非同期コードを如何に制御するかに注意を払わなければならない一方で、
Go は簡単に扱うことができます。
が非同期コードを如何に制御するかに注意を払わなければならない一方で、
Go は簡単に扱うことができます。
Go における並列処理のもうひとつの優れた面は、race detector です。
これは、非同期コードの中に複数のスレッドによる同時アクセスで起こる
データの競合があった場合に、簡単に発見できるものです。
これは、非同期コードの中に複数のスレッドによる同時アクセスで起こる
データの競合があった場合に、簡単に発見できるものです。
こちらは Go と channel を始めるのに良い情報源です。
- https://gobyexample.com/channels
- https://tour.golang.org/concurrency/2
- http://guzalexander.com/2013/12/06/golang-channels-tutorial.html
- https://www.golang-book.com/books/intro/10
- https://www.goinggo.net/2014/02/the-nature-of-channels-in-go.html
- Goroutines vs Green threads
理由5 – 速いコンパイル時間
Go で書かれた私たちの一番大きなマイクロサービスのコンパイル時間は
現在6秒です。
Go のコンパイル時間の速さは、コンパイル時間が遅いことで有名な
Java や C++ のような言語と比較して大きく優れています。
コンパイル時間が長いのでちゃんばらをする時間がありましたが、
Go にするとその時間がなくなりました。
自分が書いたコードが何のためだったのか憶えているうちに物事が
片付く方がはるかに素晴らしいです。
現在6秒です。
Go のコンパイル時間の速さは、コンパイル時間が遅いことで有名な
Java や C++ のような言語と比較して大きく優れています。
コンパイル時間が長いのでちゃんばらをする時間がありましたが、
Go にするとその時間がなくなりました。
自分が書いたコードが何のためだったのか憶えているうちに物事が
片付く方がはるかに素晴らしいです。
理由6 – チームを構築する能力
これは明らかな事実ですが、C++ や Java のような古い言語に比べて、
Go の開発者の数は多くありません。StackOverflow によると、開発者の
38% が Java を知っており、C++ は 19.3%、Go は 4.6% しか
知りませんでした。
GitHub のデータも同様の傾向を示しています。
Go は Erlang、Scala や Elixir のような言語よりも幅広く使われて
いますが、Java や C++ ほど人気はありません。
Go の開発者の数は多くありません。StackOverflow によると、開発者の
38% が Java を知っており、C++ は 19.3%、Go は 4.6% しか
知りませんでした。
GitHub のデータも同様の傾向を示しています。
Go は Erlang、Scala や Elixir のような言語よりも幅広く使われて
いますが、Java や C++ ほど人気はありません。
幸いなことに、Go はとてもシンプルで習得しやすい言語です。
Go は基本的な機能を提供するだけで、他には何もありません。
Go が導入した新しいコンセプトは、defer 文と並列処理を管理する
組み込みの goroutine と channel です。
(注 : Go はこれらのコンセプトを実装した最初の言語ではなく、
これらを普及させた最初の言語ということです。)
Go はシンプルなので、Python 、Elixir 、C++ 、Scala もしくは
Java の開発者なら誰でも1ヶ月以内に Go が使えるようになるでしょう。
Go は基本的な機能を提供するだけで、他には何もありません。
Go が導入した新しいコンセプトは、defer 文と並列処理を管理する
組み込みの goroutine と channel です。
(注 : Go はこれらのコンセプトを実装した最初の言語ではなく、
これらを普及させた最初の言語ということです。)
Go はシンプルなので、Python 、Elixir 、C++ 、Scala もしくは
Java の開発者なら誰でも1ヶ月以内に Go が使えるようになるでしょう。
私達は、他の言語と比較しても Go 開発者のチームを構築するのが
容易であることに気が付きました。もし Boulder や Amsterdam の
ような競争が激しい地域で採用活動をしているなら、
これは重要な利点です。
容易であることに気が付きました。もし Boulder や Amsterdam の
ような競争が激しい地域で採用活動をしているなら、
これは重要な利点です。
理由7 - 強いエコシステム
20人程度までのチームの話です。もし、全ての小さな機能まで
再実装しなければならないとしたら、顧客に対して価値を提供する
ことはできません。
Go は我々が使うライブラリ(Redis、RabbitMQ、PostgreSQL、
テンプレートのパース、タスクスケジューリング、語句解析、RocksDB)
をすでに用意していました。
再実装しなければならないとしたら、顧客に対して価値を提供する
ことはできません。
Go は我々が使うライブラリ(Redis、RabbitMQ、PostgreSQL、
テンプレートのパース、タスクスケジューリング、語句解析、RocksDB)
をすでに用意していました。
Go のエコシステムは、Rust もしくは Elixir のような他の新しい言語と
比較して優れています。
それはもちろん Java、 Python、Node ほど良くはありませんが、
高品質のパッケージの中からあなたの必要なものを見つけることが
できるでしょう。
比較して優れています。
それはもちろん Java、 Python、Node ほど良くはありませんが、
高品質のパッケージの中からあなたの必要なものを見つけることが
できるでしょう。
理由8 – Gofmt による強制的なコードフォーマット
Gofmt が何かというところから始めましょう。Gofmt は Go のコードを
整形のためにコンパイラに入っている素晴らしい
コマンドラインユーティリティです。
機能面から言えば、それは Python の autopep8 にとても良く似ています。
我々のほとんどはタブとスペースについての議論が好きではありません。
フォーマットが一貫していることは重要ですが、
実際のどのフォーマットを使うのかはそれほど重要ではありません。
Gofmt はコードをフォーマットするための公式ツールを用意することで、
この議論を回避しています。
整形のためにコンパイラに入っている素晴らしい
コマンドラインユーティリティです。
機能面から言えば、それは Python の autopep8 にとても良く似ています。
我々のほとんどはタブとスペースについての議論が好きではありません。
フォーマットが一貫していることは重要ですが、
実際のどのフォーマットを使うのかはそれほど重要ではありません。
Gofmt はコードをフォーマットするための公式ツールを用意することで、
この議論を回避しています。
理由9 – gRPC と Protocol Buffers
Go は Protocol Buffers と gRPC サポートが素晴らしいです。
これらの2つのツールは、RPC 経由で通信する必要のある
マイクロサービスに対して非常に機能します。
あなたは、RPC call と引数を定義した manifest を書くだけです。
サーバとクライアントのコードはこの manifest から自動的に作成されます。
作成されたコードは共に速く、非常に小さなネットワーク帯域で、
使い易いです。
これらの2つのツールは、RPC 経由で通信する必要のある
マイクロサービスに対して非常に機能します。
あなたは、RPC call と引数を定義した manifest を書くだけです。
サーバとクライアントのコードはこの manifest から自動的に作成されます。
作成されたコードは共に速く、非常に小さなネットワーク帯域で、
使い易いです。
同じ manifest から、 C++、Java、 Python、Ruby といった他の言語向けの
RPC クライアントコードを作成することができます。
そのため、クライアントとサーバで毎回ほとんど同じコードを書く必要はなく、
内部トラフィック向けの REST エンドポイントも必要ありません。
RPC クライアントコードを作成することができます。
そのため、クライアントとサーバで毎回ほとんど同じコードを書く必要はなく、
内部トラフィック向けの REST エンドポイントも必要ありません。
Go を使うことによる不利益
不利益1 – フレームワークの欠如
Go には Ruby に対する Rails、Python に対する Django、PHP に対する
Laravel のような主要なフレームワークがありません。
多くの人々が最初にフレームワークを使うべきではないと主張しているので、
これは Go コミュニティの中でも激論となっています。
いくつかのユースケースについては、この考えが正しいことに全面的に賛成です。
しかし、もしシンプルな CRUD API を構築したいときは、Django/DRF、
Rails、Laravel もしくは Phoenix を使った方がはるかに簡単です。
Laravel のような主要なフレームワークがありません。
多くの人々が最初にフレームワークを使うべきではないと主張しているので、
これは Go コミュニティの中でも激論となっています。
いくつかのユースケースについては、この考えが正しいことに全面的に賛成です。
しかし、もしシンプルな CRUD API を構築したいときは、Django/DRF、
Rails、Laravel もしくは Phoenix を使った方がはるかに簡単です。
追記: コメントで指摘があるように、Go のフレームワークは存在します。
Revel、Iris、Echo、Macaron、Buffalo などがメジャーです。
ですが、Stream のユースケースではフレームワークを利用しないことを
選んでいます。シンプルな CRUD API を提供しようとしているプロジェクト
ではフレームワークの欠如は深刻な問題でしょう。
Revel、Iris、Echo、Macaron、Buffalo などがメジャーです。
ですが、Stream のユースケースではフレームワークを利用しないことを
選んでいます。シンプルな CRUD API を提供しようとしているプロジェクト
ではフレームワークの欠如は深刻な問題でしょう。
不利益2 – エラーハンドリング
Go は、関数がエラーを返し、呼び出す側のコードがそのエラーを制御して
くれる前提でエラーハンドリングしています(もしくはそれをスタックに返す)。
このアプローチは有効ですが、ユーザーに対して具体的なエラー内容を伝える
ことはできません。errors package は、errors に context と stack trace を
加えることによってこの問題を解決します。
くれる前提でエラーハンドリングしています(もしくはそれをスタックに返す)。
このアプローチは有効ですが、ユーザーに対して具体的なエラー内容を伝える
ことはできません。errors package は、errors に context と stack trace を
加えることによってこの問題を解決します。
もう一つの問題は、エラーハンドリングを忘れてしまうことです。
errcheck や megacheck のような静的分析ツールはこれらのミスを
手軽に防ぐことができます。
errcheck や megacheck のような静的分析ツールはこれらのミスを
手軽に防ぐことができます。
これらの方法はうまく機能しますが、それが正しいとは思いません。
言語自身の適切なエラーハンドリングのサポートを期待しましょう。
言語自身の適切なエラーハンドリングのサポートを期待しましょう。
不利益3 – パッケージ管理
Go のパッケージ管理は決して完璧ではありません。デフォルトでは、
依存パッケージのバージョンを指定することができないので、
再現可能なビルドを作成する方法もありません。Python、Node、
Ruby は良いパッケージ管理のシステムを持っています。
しかしながら、適切なツールを使えば、Go のパッケージ管理は極めて
うまく機能します。
依存パッケージのバージョンを指定することができないので、
再現可能なビルドを作成する方法もありません。Python、Node、
Ruby は良いパッケージ管理のシステムを持っています。
しかしながら、適切なツールを使えば、Go のパッケージ管理は極めて
うまく機能します。
Dep を使うことで、バージョンを指定し、依存関係を管理することが
できます。それとは別に、Go で書かれた複数のプロジェクトの作業を
容易にする VirtualGo と呼ばれるオープンソースツールもあります。
できます。それとは別に、Go で書かれた複数のプロジェクトの作業を
容易にする VirtualGo と呼ばれるオープンソースツールもあります。
Python vs Go
私達は、Python で書かれたランク付けされたフィードを Go で再実装する
という実験をしました。以下の ranking method の例を見てください。
という実験をしました。以下の ranking method の例を見てください。
{
"functions": {
"simple_gauss": {
"base": "decay_gauss",
"scale": "5d",
"offset": "1d",
"decay": "0.3"
},
"popularity_gauss": {
"base": "decay_gauss",
"scale": "100",
"offset": "5",
"decay": "0.5"
}
},
"defaults": {
"popularity": 1
},
"score": "simple_gauss(time)*popularity"
}
Python も Go もこの ranking method を実装するには以下の処理が必要です。
- score 用の語句を解析する。この例では、"simple_gauss(time)*popularity"
- という文字列の部分を、activity を入力に、score を出力で返す関数に
- 変換したい。
- JSON の設定に基づいた部分関数を作成する。例えば、"decay_gauss" を
- 呼び出す "simple_gauss" を5日間、1日のオフセットと 0.3 の減衰係数の
- 尺度で生成したい。
- activity 上で field 定義されていない場合に fallback できるよう、
- "defaults" の設定を解析する。
- feed 内の全ての activities を得るため、ステップ1から関数を使う。
Python バージョンの開発には約3日かかりました。それにはコードの生成、
ユニットテストとドキュメントの作成が含まれます。
次に、コードを最適化するのに約2週間かかりました。
最適化のひとつは score expression (simple_gauss(time)*popularity) を
抽象構文木に変換することでした。
また、特定の時間に score を事前に計算するような
キャッシュロジックを実装しました。
ユニットテストとドキュメントの作成が含まれます。
次に、コードを最適化するのに約2週間かかりました。
最適化のひとつは score expression (simple_gauss(time)*popularity) を
抽象構文木に変換することでした。
また、特定の時間に score を事前に計算するような
キャッシュロジックを実装しました。
それに対し、このコードの Go バージョンの開発は、約4日かかりました。
パフォーマンスとしてはそれ以上の最適化は必要ありませんでした。
そのため、初期バージョンの開発は Python のほうが早くすみましたが、
最終的にチームでの作業量は Go ベースのほうが圧倒的に
少なくなりました。
加えて、Go のコードは高度に最適化された Python のコードよりも
約40倍速くなりました。
パフォーマンスとしてはそれ以上の最適化は必要ありませんでした。
そのため、初期バージョンの開発は Python のほうが早くすみましたが、
最終的にチームでの作業量は Go ベースのほうが圧倒的に
少なくなりました。
加えて、Go のコードは高度に最適化された Python のコードよりも
約40倍速くなりました。
以上が Python から Go に移行したことで経験したパフォーマンスに
関する1つの例です。
もちろん、単純に比較はできません:
関する1つの例です。
もちろん、単純に比較はできません:
- ranking コードは Go で書かれた最初のプロジェクトでした。
- Go のコードは Python のコードよりも後に開発されたので、
- ユースケースは良く理解されていました。
- Go の語句解析のライブラリは非常に優れていました。
我々のシステムの他のコンポーネントは、Go で開発する方が Python に
比べて実質的に時間がかかりました。一般的には、Go での開発は若干努力が
必要です。しかし、パフォーマンスのためにコードを最適化する時間は
圧倒的に少ないでしょう。
比べて実質的に時間がかかりました。一般的には、Go での開発は若干努力が
必要です。しかし、パフォーマンスのためにコードを最適化する時間は
圧倒的に少ないでしょう。
Elixir vs Go
私たちが比較したもう一つの言語は Elixir です。Elixir は
Erlang 仮想マシン上でビルドされます。Elixir は興味深い言語で、
チームメンバーの1人が Erlang を多く経験しているので、
やってみることになりました。
Erlang 仮想マシン上でビルドされます。Elixir は興味深い言語で、
チームメンバーの1人が Erlang を多く経験しているので、
やってみることになりました。
私たちのユースケースでは、Go の素のパフォーマンスのほうが遥かに
上でした。
Go も Elixir も何千もの並行処理のリクエストをこなすでしょう。
しかし、個別のリクエストのパフォーマンスを見れば、
Go はユースケース上、実質的に速いです。
Elixir よりも Go を選ぶもう一つの理由は、エコシステムです。
私たちが要求したコンポーネントでは、Go はより成熟したライブラリを
持っていましたが、多くのケースで Elixir のライブラリは本番運用の
準備が整っていませんでした。
また、Elixir で作業する開発者をトレーニングしたり見つけたりするのが
難しいのです。
上でした。
Go も Elixir も何千もの並行処理のリクエストをこなすでしょう。
しかし、個別のリクエストのパフォーマンスを見れば、
Go はユースケース上、実質的に速いです。
Elixir よりも Go を選ぶもう一つの理由は、エコシステムです。
私たちが要求したコンポーネントでは、Go はより成熟したライブラリを
持っていましたが、多くのケースで Elixir のライブラリは本番運用の
準備が整っていませんでした。
また、Elixir で作業する開発者をトレーニングしたり見つけたりするのが
難しいのです。
これらの理由から Go を採用することにしました。
ですが、Elixir 向けの Phoenix フレームワークは素晴らしいように見えますし、
見る価値はあります。
ですが、Elixir 向けの Phoenix フレームワークは素晴らしいように見えますし、
見る価値はあります。
結論
Go は並列処理をサポートするとても効率の良い言語です。
C++ や Java のような言語とほぼ同じくらい速いです。
Python や Ruby に比べて開発するのに少し時間がかかりますが、
コードを最適化するのにとても多くの時間を節約できるでしょう。
C++ や Java のような言語とほぼ同じくらい速いです。
Python や Ruby に比べて開発するのに少し時間がかかりますが、
コードを最適化するのにとても多くの時間を節約できるでしょう。
Stream は、2億人以上のエンドーユーザー向けにニュースフィードを
提供する小さな開発チームです。優れたエコシステム、
新規開発者の容易な参入、高速なパフォーマンス、並列制御のサポート、
そして生産的なプログラミング環境の組み合わせにより、
Go は素晴らしい選択肢といえるでしょう。
提供する小さな開発チームです。優れたエコシステム、
新規開発者の容易な参入、高速なパフォーマンス、並列制御のサポート、
そして生産的なプログラミング環境の組み合わせにより、
Go は素晴らしい選択肢といえるでしょう。
Stream は、ダッシュボード、ウェブサイト、
そしてパーソナライズされた feed に Python を使用しています。
私たちはすぐに Python に別れを告げるつもりはありませんが、
パフォーマンスを重視するコードは Go で書かれることになるでしょう。
もし Go についてもっと知りたければ、
下記のブログ記事をチェックしてください。
Stream についてもっと知るために、このチュートリアルが素晴らしい
出発点になります。
そしてパーソナライズされた feed に Python を使用しています。
私たちはすぐに Python に別れを告げるつもりはありませんが、
パフォーマンスを重視するコードは Go で書かれることになるでしょう。
もし Go についてもっと知りたければ、
下記のブログ記事をチェックしてください。
Stream についてもっと知るために、このチュートリアルが素晴らしい
出発点になります。
Go への移行についてもっと知る
- https://movio.co/en/blog/migrate-Scala-to-Go/
- https://hackernoon.com/why-i-love-golang-90085898b4f7
- https://sendgrid.com/blog/convince-company-go-golang/
- https://dave.cheney.net/2017/03/20/why-go
0 件のコメント:
コメントを投稿