GoとTypeScriptをユースケースに基づき比較してみた
これは、Go 4 Advent Calendar 2020 の24日目の記事です。
これはなに
GoとTypeScriptをユースケースに基づき比較してみた記事です。自分なりに言語を利用している上での考察(とよんでいいのか不安ですが!)です。考察だけ読みたい方は、こちらから。
背景
筆者は、GoとTypeScriptが好きなサーバーサイドエンジニアです。今年は、業務でTypeScriptを扱う割合が多い一年でした。一方で、Goで作ったOSSがGoogleのFirebase Open Sourceに掲載されたりもしました。
そんな筆者が、GoとTypeScriptをユースケースに基づいて比較をしてみます。これらを比較する理由として、両言語は「型付・コンパイルする」という点では似ている一方で、特徴が異なると感じているからです。これらの違いを実際のコードに基づき考察してみます。
本記事は、限られたユースケースに基づく考察をしているため、かなり偏りがある内容となっています。言語選定のために本記事を見れる場合には、あくまで想定されているケースと本記事が一致しているかをご留意ください。
環境
TypeScriptは、下記の前提で記述します。
// tsconfig.json
"module": "commonjs",
"target": "es2017"
$ node -v
v15.4.0
ユースケースごとの実装例
「外部APIの利用」のユースケースにおける実装例を示します。
外部APIを扱う
HTTPリクエスト
利用するライブラリ、リクエスト生成(リクエストパラメータ生成・Header設定)、リトライ処理などに違いを見ると面白いです。
GoでHTTPリクエスト
import (
// 標準ライブラリをつかってHTTP通信することが多いです
// https://pkg.go.dev/net/http
"net/url"
// ...
)
// GET
resp, err := http.Get("http://example.com/")
// POST
resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf)
// Get は、Client.Do()のサンプル関数
// より細かな制御を行いたい時には、http.Client.Do() をcallする
func (c *XXClient) Get(url string) ([]byte, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
// Headerの設定
req.Header.Set("content-type", "application/x-www-form-urlencoded")
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if c.IsErrorRetirable(resp) {
// TODO: リトライ処理
}
if resp.StatusCode != http.StatusOK {
return nil, errors.New("invalid http status")
}
return ioutil.ReadAll(resp.Body)
}
TypeScriptでHTTPリクエスト
// 他にもライブラリはありますが、axiosがメジャーな気がします
// https://github.com/axios/axios#example
import axios from "axios";
import axiosRetry from "axios-retry";
import qs from 'qs'; //POSTパラメータ作成用
// GET
const resp = await axios.get('/user')
// POST
const resp = await axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
})
// 細かな制御: configを使う
// https://github.com/axios/axios#request-config
type Response = {} //TODO:レスポンスの型定義
const postUser = async ():Promise<Response | null> => {
const url = '/user'
const data = { 'bar': 123 };
const options = {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
data: qs.stringify(data),
url,
};
axiosRetry(axios, {
retries: 3,
retryDelay: axiosRetry.exponentialDelay
}); // 500系エラーでリトライ
try {
const resp = await axios(options);
if (resp.status != 200){
throw new Error(`invalid status: ${resp.status}`)
}
return resp.body
} catch(e) {
//エラーハンドリング
console.log(e)
}
return null
}
JSONをパースする
// body io.Reader
// outの構造体へデコードする
json.NewDecoder(body).Decode(out)
GoでJSONパース
package main
import (
"encoding/json"
"io"
"log"
"strings"
)
// Colors is response of api
type Colors struct {
Colors []struct {
Value string `json:"value"`
} `json:"colors"`
}
func decodeBody(body io.Reader, out interface{}) error {
decoder := json.NewDecoder(body)
return decoder.Decode(out)
}
func run() error {
var colors Colors
respBody := strings.NewReader(`{
"colors": [
{
"value": "#3F6B95"
}
]
}`)
err := decodeBody(respBody, &colors)
if err != nil {
return err
}
log.Printf("get colors: %#v", colors)
return nil
}
func main() {
if err := run(); err != nil {
log.Fatalf("err: %v", err)
}
# OUTPUT: get colors: main.Colors{Colors:[]struct { Value string "json:\"value\"" }{struct { Value string "json:\"value\"" }{Value:"#3F6B95"}}}
リクエストの並列処理
APIを並列リクエストし、エラーハンドリングをする例です。
Goでリクエストの並列処理
// multi_request.go
package main
import (
"fmt"
"log"
"math/rand"
"net/http"
"time"
"go.uber.org/multierr"
"golang.org/x/sync/errgroup"
)
func get(query string) (string, error) {
waitSec := rand.Intn(10)
log.Printf("%s waiting ... %v sec", query, waitSec)
time.Sleep(time.Duration(waitSec) * 1000 * time.Millisecond)
url := fmt.Sprintf("https://ja.wikipedia.org/wiki/%s", query)
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("invalid status code: %v", resp.StatusCode)
}
respStr := fmt.Sprintf("query:%s, status: %v", query, resp.StatusCode)
log.Printf(" => リクエスト結果:%s", respStr)
return respStr, nil
}
func multiRequest(queries []string) error {
var err error
results := make(chan string, len(queries))
eg := errgroup.Group{}
for _, query := range queries {
q := query
requestFunc := func() error {
respStr, err := get(q)
if err != nil {
return err
}
results <- respStr
return nil
}
eg.Go(requestFunc)
}
if errLocal := eg.Wait(); errLocal != nil {
err = multierr.Append(err, errLocal)
}
close(results)
errors := multierr.Errors(err)
for _, err := range errors {
log.Printf("ここで個別のハンドリングを行う %s /%v", err, len(errors))
}
for r := range results {
log.Printf("レスポンス結果で何かする: %s", r)
}
return err
}
func run() error {
rand.Seed(time.Now().Unix())
queries := []string{
"Go",
"JavaScript",
"ruby",
"python",
"aaaaaaa",
}
return multiRequest(queries)
}
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
実行結果です。wait時間が短いものからレスポンスが取得できているので、並列処理ができていることがわかりますね。
$ go run multi_request.go
2020/12/19 16:20:44 aaaaaaa waiting ... 0 sec
2020/12/19 16:20:44 Go waiting ... 3 sec
2020/12/19 16:20:44 python waiting ... 9 sec
2020/12/19 16:20:44 ruby waiting ... 7 sec
2020/12/19 16:20:44 JavaScript waiting ... 0 sec
2020/12/19 16:20:45 => リクエスト結果:query:JavaScript, status: 200
2020/12/19 16:20:48 => リクエスト結果:query:Go, status: 200
2020/12/19 16:20:52 => リクエスト結果:query:ruby, status: 200
2020/12/19 16:20:54 => リクエスト結果:query:python, status: 200
2020/12/19 16:20:54 ここで個別のハンドリングを行う invalid status code: 404 /1
2020/12/19 16:20:54 レスポンス結果で何かする: query:JavaScript, status: 200
2020/12/19 16:20:54 レスポンス結果で何かする: query:Go, status: 200
2020/12/19 16:20:54 レスポンス結果で何かする: query:ruby, status: 200
2020/12/19 16:20:54 レスポンス結果で何かする: query:python, status: 200
2020/12/19 16:20:54 invalid status code: 404
exit status 1
TypeScriptでリクエストの並列処理
// src/multiRequest.ts
import axios from "axios";
import { random } from "lodash";
import * as winston from "winston";
const createLogger = () => {
const MESSAGE = Symbol.for("message");
const date = new Date().toISOString().split("T")[0];
const jsonFormatter = (logEntry: any) => {
const base = { timestamp: new Date() };
const json = Object.assign(base, logEntry);
logEntry[MESSAGE] = JSON.stringify(json);
return logEntry;
};
return winston.createLogger({
level: "info",
format: winston.format(jsonFormatter)(),
transports: [
new winston.transports.File({ filename: `logs/${date}.log` }),
new winston.transports.Console(),
],
});
};
const logger = createLogger();
const sleep = (msec: number) => {
return new Promise((resolve) => setTimeout(resolve, msec));
};
const get = async (query: string): Promise<string> => {
const url = `https://ja.wikipedia.org/wiki/${query}`;
const sec = random(1, 10);
logger.info(`${query}: ${sec} sec waiting...`);
await sleep(sec * 1000);
const resp = await axios({ method: "GET", url });
const respStr = `query: ${query}, status: ${resp.status}`;
logger.info(` => レスポンス取得完了: ${respStr}`);
return respStr;
};
const multiRequest = async (queries: string[]): Promise<void> => {
const results: string[] = [];
const errors: Error[] = [];
await Promise.all(
queries.map(async (query) => {
try {
const result = await get(query);
results.push(result);
} catch (e) {
errors.push(e);
}
})
);
errors.forEach((e) => {
logger.info(`エラーごとにハンドリング: ${e?.message}`);
});
results.forEach((result) => {
logger.info(`レスポンス結果で何かする :${result}`);
});
};
const main = async (): Promise<void> => {
const queries: string[] = ["Go", "JavaScript", "ruby", "python", "aaaaaaa"];
await multiRequest(queries);
};
main();
実行結果です。こちらもレスポンスの取得順から、並列処理ができていることがわかります。
$ ts-node src/multiRequest.ts
{"timestamp":"2020-12-19T07:39:49.956Z","message":"Go: 6 sec waiting...","level":"info"}
{"timestamp":"2020-12-19T07:39:49.958Z","message":"JavaScript: 3 sec waiting...","level":"info"}
{"timestamp":"2020-12-19T07:39:49.958Z","message":"ruby: 9 sec waiting...","level":"info"}
{"timestamp":"2020-12-19T07:39:49.958Z","message":"python: 7 sec waiting...","level":"info"}
{"timestamp":"2020-12-19T07:39:49.958Z","message":"aaaaaaa: 7 sec waiting...","level":"info"}
{"timestamp":"2020-12-19T07:39:53.938Z","message":" => レスポンス取得完了: query: JavaScript, status: 200","level":"info"}
{"timestamp":"2020-12-19T07:39:56.363Z","message":" => レスポンス取得完了: query: Go, status: 200","level":"info"}
{"timestamp":"2020-12-19T07:39:57.783Z","message":" => レスポンス取得完了: query: python, status: 200","level":"info"}
{"timestamp":"2020-12-19T07:39:59.805Z","message":" => レスポンス取得完了: query: ruby, status: 200","level":"info"}
{"timestamp":"2020-12-19T07:39:59.806Z","message":"エラーごとにハンドリング: Request failed with status code 404","level":"info"}
{"timestamp":"2020-12-19T07:39:59.806Z","message":"レスポンス結果で何かする :query: JavaScript, status: 200","level":"info"}
{"timestamp":"2020-12-19T07:39:59.807Z","message":"レスポンス結果で何かする :query: Go, status: 200","level":"info"}
{"timestamp":"2020-12-19T07:39:59.807Z","message":"レスポンス結果で何かする :query: python, status: 200","level":"info"}
{"timestamp":"2020-12-19T07:39:59.807Z","message":"レスポンス結果で何かする :query: ruby, status: 200","level":"info"}
補足
TypeScriptにおけるasync/awaitもれで予期せぬ挙動をする例
TypeScriptにおけるasync/awaitもれ
// src/asyncAwaitError.ts
// 正しくは: const get = async (query: string):Promise<string> => {
// 誤り: const get = (query: string) => {
import axios from "axios";
import { random } from "lodash";
import * as winston from "winston";
const createLogger = () => {
const MESSAGE = Symbol.for("message");
const date = new Date().toISOString().split("T")[0];
const jsonFormatter = (logEntry: any) => {
const base = { timestamp: new Date() };
const json = Object.assign(base, logEntry);
logEntry[MESSAGE] = JSON.stringify(json);
return logEntry;
};
return winston.createLogger({
level: "info",
format: winston.format(jsonFormatter)(),
transports: [
new winston.transports.File({ filename: `logs/${date}.log` }),
new winston.transports.Console(),
],
});
};
const logger = createLogger();
const sleep = (msec: number) => {
return new Promise((resolve) => setTimeout(resolve, msec));
};
const get = (query: string) => {
const url = `https://ja.wikipedia.org/wiki/${query}`;
const sec = random(1, 10);
logger.info(`${query}: ${sec} sec waiting...`);
sleep(sec * 1000);
const resp = axios({ method: "GET", url });
const respStr = `query: ${query}, status: ${resp}`; //🚨実はここで resp.status にアクセスしようとすると型エラーには気付ける
logger.info(` => レスポンス取得完了: ${respStr}`);
return respStr;
};
const multiRequest = async (queries: string[]): Promise<void> => {
const results: string[] = [];
const errors: Error[] = [];
await Promise.all(
queries.map(async (query) => {
try {
const result = get(query); // ここでasync/awaitもれ
results.push(result);
} catch (e) {
errors.push(e);
}
})
);
errors.forEach((e) => {
logger.info(`エラーごとにハンドリング: ${e?.message}`);
});
results.forEach((result) => {
logger.info(`レスポンス結果で何かする :${result}`);
});
};
const main = async (): Promise<void> => {
const queries: string[] = ["Go", "JavaScript", "ruby", "python", "aaaaaaa"];
await multiRequest(queries);
};
main();
コンパイルは成功しますが、実行するとエラーになります。
$ ts-node src/asyncAwaitError.ts
{"timestamp":"2020-12-19T08:16:52.328Z","message":"Go: 5 sec waiting...","level":"info"}
{"timestamp":"2020-12-19T08:16:52.331Z","message":" => レスポンス取得完了: query: Go, status: [object Promise]","level":"info"}
{"timestamp":"2020-12-19T08:16:52.331Z","message":"JavaScript: 7 sec waiting...","level":"info"}
{"timestamp":"2020-12-19T08:16:52.332Z","message":" => レスポンス取得完了: query: JavaScript, status: [object Promise]","level":"info"}
{"timestamp":"2020-12-19T08:16:52.332Z","message":"ruby: 3 sec waiting...","level":"info"}
{"timestamp":"2020-12-19T08:16:52.332Z","message":" => レスポンス取得完了: query: ruby, status: [object Promise]","level":"info"}
{"timestamp":"2020-12-19T08:16:52.332Z","message":"python: 7 sec waiting...","level":"info"}
{"timestamp":"2020-12-19T08:16:52.332Z","message":" => レスポンス取得完了: query: python, status: [object Promise]","level":"info"}
{"timestamp":"2020-12-19T08:16:52.332Z","message":"aaaaaaa: 1 sec waiting...","level":"info"}
{"timestamp":"2020-12-19T08:16:52.332Z","message":" => レスポンス取得完了: query: aaaaaaa, status: [object Promise]","level":"info"}
{"timestamp":"2020-12-19T08:16:52.373Z","message":"レスポンス結果で何かする :query: Go, status: [object Promise]","level":"info"}
{"timestamp":"2020-12-19T08:16:52.373Z","message":"レスポンス結果で何かする :query: JavaScript, status: [object Promise]","level":"info"}
{"timestamp":"2020-12-19T08:16:52.373Z","message":"レスポンス結果で何かする :query: ruby, status: [object Promise]","level":"info"}
{"timestamp":"2020-12-19T08:16:52.373Z","message":"レスポンス結果で何かする :query: python, status: [object Promise]","level":"info"}
{"timestamp":"2020-12-19T08:16:52.373Z","message":"レスポンス結果で何かする :query: aaaaaaa, status: [object Promise]","level":"info"}
.../node_modules/axios/lib/core/createError.js:16
var error = new Error(message);
^
Error: Request failed with status code 404
...
Goにおける並行処理
前提として、「並行処理」と「並列処理」の違いについては、koronさんの君たちの「並行」の理解は間違ってるがわかりやすいです。抜粋しますと、下記の違いがあります。
- 並行計算=同時に実行すること
- 並列計算≒同等のタスクを並列計算すること
「リクエストの並列処理」で紹介したものは、同じ処理を並列計算したものでした。
Goではgoroutine
というしくみで go func() {}()
と記述することで、並行処理を実現できます。
Goにおける並行処理
package main
import (
"fmt"
"sync"
"time"
)
func wgSleep(wg *sync.WaitGroup, sec time.Duration) {
time.Sleep(time.Second * sec)
wg.Done()
}
func main() {
var wg sync.WaitGroup
start := time.Now()
wg.Add(2)
go func() {
fmt.Printf("処理Aを行う")
wgSleep(&wg, 1)
}()
go func() {
fmt.Printf("処理Bを行う")
wgSleep(&wg, 2)
}()
wg.Wait()
end := time.Now()
fmt.Println(end.Sub(start))
}
考察
HTTP通信のライブラリ
- Goは標準ライブラリでHTTP通信のライブラリを備えている。
- TypeScriptはサードパーティ製のライブラリを利用する必要がある。たとえばリトライをするなどの場合に、ライブラリの利用方法に成熟する必要がある(ので、利用するライブラリが異なると再学習が必要となってしまう)
これらの特徴は、Goのテストコードに対する思想にも現れていると思います。
「rubyのRSpec」や「JavaScriptのJest」などのように他言語ではテストコード専用の言語やライブラリがあるのに対して、Goはテストコードのための学習が不要(標準ライブラリのtestingを使うだけ)である点は、個人的にとても好きです。
JSONの扱い
- JSONを扱う際の記述量が少ないのは、TypeScript
- 裏を返せば、Goは型を厳密に扱うため実装時・リリース前に、問題に気づきやすい面もあると思いました
エラーハンドリングと静的解析
- Goは愚直に
err
を返してそのハンドリングを行う必要がある。記述漏れがあっても静的解析で気づけるようになっている [1] - 一方でTypeScriptは、例外を発生させるパターン・例外を返すパターン・errフィールドに値を返すパターンなど複数の方法がある。特に例外を発生させた場合にはハンドリング(try/catch)が必要だが、記述もれがあると後続の処理が実行できない。try/catchの記述漏れがあったときに、静的解析で気付けるとよいなぁとおもいますが、筆者はそのようなlint処理をしりません。
- 似た話で、TypeScript(というかJavaScript)の
async/await
ですが、適切に記述しないと予期せぬ動作となります。「TypeScriptにおけるasync/awaitもれで予期せぬ挙動をする例」であげたパターンですね。このような場合についても、静的解析で気付けるといいなぁ[2]と感じます。
並列処理
- どちらの言語でも、並列処理を実装できた
- 一方で、並列 処理(≒同等のタスクを並列計算する処理)を実装するのは、Goが圧倒的に優位である (まさにこれが、Goの特徴かなと実感できます)
お知らせ
Goは無関係なのですが😢、お知らせす。
12/26〜の技術書典10で、「Firestore Testing - なぜテストを書くのか、どう書くのかがよくわかる」(TypeScriptとFirestoreのテストに関するもの)という書籍を発売予定です。お手にとっていただけますと幸いです。
👉👉 サークルページ 👈👈
-
golangci-lint経由で
errcheck
のlintをかけることが多いです。 ↩︎ -
値にアクセス(先に示したコードの例だと
resp.status
へのアクセス)しようとすると、型エラーで気付けるといえば気付けます。 ↩︎
Discussion