👀

GoとTypeScriptをユースケースに基づき比較してみた

2020/12/23に公開

これは、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のテストに関するもの)という書籍を発売予定です。お手にとっていただけますと幸いです。

👉👉 サークルページ 👈👈




脚注
  1. golangci-lint経由でerrcheck のlintをかけることが多いです。 ↩︎

  2. 値にアクセス(先に示したコードの例だとresp.statusへのアクセス)しようとすると、型エラーで気付けるといえば気付けます。 ↩︎

Discussion