2022/01/27


動的型付けプログラミング言語や、class を持つようなプログラミング言語を使う場合、そのフィールドメンバの定義順などを意識する事は少ないかもしれません。

Go は struct という、C言語が持っている構造体と同じ様にメモリ配置され、構造体そのものをレシーバとして、メソッドを定義しつつプログラミングを行います。struct はデータ構造そのものを示し、それが大きくなるにつれ、当然ながらメモリ使用量も多くなります。

さて、例えば以下のコードを実行すると、struct 100 個分のサイズは幾らになるでしょうか。

package main

import (
    "fmt"
    "unsafe"
)

type Account struct {
    TimeZone struct {
        Name       string `json:"name"`
        UtcOffset  int    `json:"utc_offset"`
        TzinfoName string `json:"tzinfo_name"`
    } `json:"time_zone"`
    Protected                bool   `json:"protected"`
    ScreenName               string `json:"screen_name"`
    AlwaysUseHTTPS           bool   `json:"always_use_https"`
    UseCookiePersonalization bool   `json:"use_cookie_personalization"`
    SleepTime                struct {
        Enabled   bool        `json:"enabled"`
        EndTime   interface{} `json:"end_time"`
        StartTime interface{} `json:"start_time"`
    } `json:"sleep_time"`
    GeoEnabled                bool   `json:"geo_enabled"`
    Language                  string `json:"language"`
    DiscoverableByEmail       bool   `json:"discoverable_by_email"`
    DiscoverableByMobilePhone bool   `json:"discoverable_by_mobile_phone"`
    DisplaySensitiveMedia     bool   `json:"display_sensitive_media"`
    AllowContributorRequest   string `json:"allow_contributor_request"`
    AllowDmsFrom              string `json:"allow_dms_from"`
    AllowDmGroupsFrom         string `json:"allow_dm_groups_from"`
    SmartMute                 bool   `json:"smart_mute"`
    TrendLocation             []struct {
        Name        string `json:"name"`
        CountryCode string `json:"countryCode"`
        URL         string `json:"url"`
        Woeid       int    `json:"woeid"`
        PlaceType   struct {
            Name string `json:"name"`
            Code int    `json:"code"`
        } `json:"placeType"`
        Parentid int    `json:"parentid"`
        Country  string `json:"country"`
    } `json:"trend_location"`
}

func main() {
    var accounts [100]Account
    fmt.Println(unsafe.Sizeof(accounts))
}

実行すると 22400 (22kb) と表示されます。前述の様に、Go の struct はC言語と同じ様にメモリ上に配置されます。したがって、フィールドの配置順を気を付けるだけでメモリを節約できる事になります。以下のコマンドを実行して fieldalignment をインストールして下さい。

go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest

struct が定義されているソースファイルを引数に指定するか、以下の様に実行して下さい。

fieldalignment .

すると以下の様に表示されます。

.../main.go:8:14: struct of size 224 could be 192
.../main.go:9:11: struct with 32 pointer bytes could be 24
.../main.go:18:27: struct with 40 pointer bytes could be 32
.../main.go:32:30: struct with 96 pointer bytes could be 72

一般的に実行バイナリを生成するプログラミング言語は、データをメモリに載せる際にアクセスしやすいようにワードサイズ(コンピュータがメモリを読み書きする際の単位)の倍数の位置にデータを配置します。このためワードサイズに満たないフィールドの直後に、パディングという隙間を意図的に挿れることで調整しています。これにより上記の様に改善可能な空白が生まれてしまいます。

fieldalignment コマンドはフィールドの定義順を変える事で、メモリ上のアライメントにうまく配置できる様にするツールです。fieldalignment に -fix フラグを付けて実行すると、実際に struct が書き換えられます。

fieldalignment -fix .

書き換えられたソースコードは以下の通り。

package main

import (
    "fmt"
    "unsafe"
)

type Account struct {
    SleepTime struct {
        Enabled   bool        `json:"enabled"`
        EndTime   interface{} `json:"end_time"`
        StartTime interface{} `json:"start_time"`
    } `json:"sleep_time"`
    TimeZone struct {
        Name       string `json:"name"`
        UtcOffset  int    `json:"utc_offset"`
        TzinfoName string `json:"tzinfo_name"`
    } `json:"time_zone"`
    AllowDmGroupsFrom       string `json:"allow_dm_groups_from"`
    ScreenName              string `json:"screen_name"`
    AllowDmsFrom            string `json:"allow_dms_from"`
    AllowContributorRequest string `json:"allow_contributor_request"`
    Language                string `json:"language"`
    TrendLocation           []struct {
        Name        string `json:"name"`
        CountryCode string `json:"countryCode"`
        URL         string `json:"url"`
        Woeid       int    `json:"woeid"`
        PlaceType   struct {
            Name string `json:"name"`
            Code int    `json:"code"`
        } `json:"placeType"`
        Parentid int    `json:"parentid"`
        Country  string `json:"country"`
    } `json:"trend_location"`
    GeoEnabled                bool `json:"geo_enabled"`
    DiscoverableByMobilePhone bool `json:"discoverable_by_mobile_phone"`
    DisplaySensitiveMedia     bool `json:"display_sensitive_media"`
    UseCookiePersonalization  bool `json:"use_cookie_personalization"`
    AlwaysUseHTTPS            bool `json:"always_use_https"`
    Protected                 bool `json:"protected"`
    SmartMute                 bool `json:"smart_mute"`
    DiscoverableByEmail       bool `json:"discoverable_by_email"`
}

func main() {
    var accounts [100]Account
    fmt.Println(unsafe.Sizeof(accounts))
}

実行すると struct 100 個のサイズは 19200 (19kb) になりました。struct 内のフィールドの定義順に意味がある場合には使えませんが、単に JSON を出力するだけであれば試してみては如何でしょうか。

Posted at by



2022/01/08


技術評論社様、執筆者の皆様、献本ありがとうございました。

本誌のサブタイトルは

入門書では得られないノウハウはコードにある

です。

ちなみに筆者も似た様な書籍「みんなの Go 言語」を共著で執筆しましたが、本書はこちらの拡充版とも言えるでしょう。

「エキスパートたちのGo言語」は20名もの共著で色々なノウハウが沢山掲載されています。「みんなの Go 言語」は7名の共著で個々の特色を色濃く出せたと思いますので、どちらも良い作品だと思います。

昨今の Go 言語は、日々ライブラリが更新され、新しいライブラリが生まれ、トレンドが更新され、古いライブラリが使われなくなるという、細胞が角質となり剥がれ落ちまた新しい細胞が生まれる様な循環を繰り返しています。

その様な中でこういった作品を出すという事は、言ってみればリスキーな事になり得ますし、「みんなの Go 言語」を書いた際にも感じていました。しかしこういった情報をきちんと纏まった形で公開する事は非常に大切で、体系立てて読める事はとても素晴らしい事だと再認識しました。

本書の読者レベルで言うと、Go の基本的なコードが読み書き出来て、ライブラリの選定もある程度はできる、中級者くらいかなと思います。前述の様に最近の Go はツールやライブラリの入れ替わりが速い状況ですので、開発者にキャッチアップの能力が必要になります。

  • ツールやライブラリのトレンド
  • CI/CD 周りの扱い方
  • セキュリティfixのキャッチアップ
  • AWS/GCP などクラウドのトレンド

これら何れも Go 言語に限った話ではないのですが、Go 言語の場合はクラウドで使われる事が多く、自動化もしやすい為、特にツール類や CI/CD 周りに気を使わないといけない事が多いです。本書ではそういった内容を「2021 年の Go 言語」としてうまく書かれているなと思いました。

例えば通常の言語ではあまり気にされないかもしれない「リトライ処理」、クラウドで使われる事を意識してずいぶん慎重に扱われます。バックオフを意識したライブラリだけでも沢山公開されています。本書の中でもリトライ処理だけで10ページも割かれています。context の使い方やリトライ処理といった、基本的な使い方だけでなく、cgo を扱う際のノウハウ、GitHub Actions を扱う上でのノウハウ、ウェブアプリを作る際に必要な JWT/OAuth といった認証周り、WebAssembly など、色々なカテゴリで書かれています。

興味のある方は、ぜひ一度、目次とその章で割かれているページ数を参照されるのが良いと思います。1点、牽引が少ない気はしました。もしかしたら改訂版などで改良されるかもしれないですね。

本書の内容に関して、全て知ってはいなくても良いと思いますが、読んで「あー、あれね」くらいで理解が出来れば Go 言語のエキスパートと呼ばれても良いのではないでしょうか。Go 言語を使ってお仕事をされておられる方であれば、買って間違いなしの書籍だと思います。

Posted at by



2021/12/24


Go の http パッケージの Request.Body はこれまで最大バイト数を指定できなかったので、巨大なファイルをアップロードするといった DoS 攻撃を心配するのであれば、各ハンドラの中で独自でサイズ制限しながら読み込む必要がありました。Go 1.18 から http.MaxBytesHandler が入ったので簡単にサイズ制限をする事ができる様になりました。

MaxBytesHandler は http.Handler として提供されるので、例えば labstack/echo であれば以下の様に、各ハンドラを直接修正する事なく、簡単に導入して使う事ができます。

package main

import (
    "encoding/json"
    "fmt"
    "net/http"

    "github.com/labstack/echo/v4"
)

func middleware(next http.Handler) http.Handler {
    // MaxBytesHandler で wrap する
    return http.MaxBytesHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        next.ServeHTTP(w, r)
    }), 4096)
}

func main() {
    e := echo.New()

    // ココ
    e.Use(echo.WrapMiddleware(middleware))

    e.GET("/"func(c echo.Context) error {
        return c.String(http.StatusOK, "")
    })

    e.POST("/"func(c echo.Context) error {
        var v interface{}
        err := json.NewDecoder(c.Request().Body).Decode(&v)
        if err != nil {
            return err
        }
        fmt.Println(v)
        return c.String(http.StatusOK, "")
    })
    e.Logger.Fatal(e.Start(":8989"))
}

試しに大きな JSON を POST すると、500 エラーが返りました。イイカンジです。

$ curl -i -X POST -H "Content-Type: application/json" -d @bar.json http://127.0.0.1:8989/
HTTP/1.1 100 Continue

HTTP/1.1 500 Internal Server Error
Content-Type: application/json; charset=UTF-8
Date: Thu, 23 Dec 2021 15:55:30 GMT
Content-Length: 36
Connection: close

{"message":"Internal Server Error"}
Posted at by