Works by

Ren's blog

@rennnosuke_rk 技術ブログです

【Golang】Golang : `runtime` パッケージを使用したモニタリング

Golang のメモリ使用量やゴルーチン数など、実行時ランタイムに関係するステータスを観測するには、 runtime パッケージを使用する。

Memory

var m runtime.MemStats
runtime.ReadMemStats(&m)

// ヒープ上に割り当てられたオブジェクト累積メモリ量
fmt.Printf("MAlloc : %v\n", humanize.Bytes(m.Mallocs))
// ヒープ上から開放されたオブジェクト数
fmt.Printf("Frees : %v\n", m.Frees)

// ヒープ上に割り当てられたオブジェクトメモリ量
fmt.Printf("Alloc : %v\n", humanize.Bytes(m.Alloc))
fmt.Printf("HeapAlloc : %v\n", humanize.Bytes(m.HeapAlloc))
// ヒープ上に割り当てられたオブジェクトメモリ量。ただし開放されたオブジェクト分も含む
fmt.Printf("TotalAlloc : %v\n", humanize.Bytes(m.TotalAlloc))

// OSから割り当てられたプロセスの総メモリ量
// ヒープ + スタック + その他
fmt.Printf("Sys : %v\n", humanize.Bytes(m.Sys))

// ポインタのルックアップ数
fmt.Printf("Lookups : %v\n", m.Lookups)

// 到達可能、あるいはGCによって解放されていないヒープオブジェクトメモリ量
fmt.Printf("HeapAlloc : %v\n", humanize.Bytes(m.HeapAlloc))
// 未使用ヒープ領域メモリ量
fmt.Printf("HeapIdle : %v\n", humanize.Bytes(m.HeapIdle))
// 使用中ヒープ領域メモリ量
fmt.Printf("HeapInuse : %v\n", humanize.Bytes(m.HeapInuse))
// OSに返却される物理メモリ量
fmt.Printf("HeapReleased : %v\n", humanize.Bytes(m.HeapReleased))
// ヒープに割り当てられたオブジェクト量
fmt.Printf("HeapObjects : %v\n", m.HeapObjects)

// 使用中スタック領域メモリ量
fmt.Printf("StackInuse : %v\n", humanize.Bytes(m.StackInuse))
// OSから割り当てられたスタック領域メモリ量
fmt.Printf("StackSys : %v\n", humanize.Bytes(m.StackSys))

// 割り当てられたmspan構造体バイト数
fmt.Printf("MSpanInuse : %v\n", humanize.Bytes(m.MSpanInuse))
// OSから取得したmspan構造体バイト数
fmt.Printf("MSpanSys : %v\n", humanize.Bytes(m.MSpanSys))

// 割り当てられたmcache構造体バイト数
fmt.Printf("MCacheInuse : %v\n", humanize.Bytes(m.MCacheInuse))
// OSから取得したmcache構造体バイト数
fmt.Printf("MCacheSys : %v\n", humanize.Bytes(m.MCacheSys))

MAlloc : 271 B
Frees : 4
Alloc : 179 kB
HeapAlloc : 179 kB
TotalAlloc : 179 kB
Sys : 70 MB
Lookups : 0
HeapAlloc : 179 kB
HeapIdle : 66 MB
HeapInuse : 418 kB
HeapReleased : 66 MB
HeapObjects : 267
StackInuse : 229 kB
StackSys : 229 kB
MSpanInuse : 6.1 kB
MSpanSys : 16 kB
MCacheInuse : 21 kB
MCacheSys : 33 kB

Goroutine 数

var m runtime.MemStats
runtime.ReadMemStats(&m)

// 3 goroutine
go func() { time.Sleep(time.Second * 10) }()
go func() { time.Sleep(time.Second * 10) }()
go func() { time.Sleep(time.Second * 10) }()

// 呼び出し時存在するゴルーチン数(main + 3)
fmt.Printf("Goroutine : %v\n", runtime.NumGoroutine()) // n-GoroutineProfile : 4

ゴルーチン別メモリプロファイル

ゴルーチン毎のスタックトレースを uint32 配列で返す。

var m runtime.MemStats
runtime.ReadMemStats(&m)

sr := []runtime.StackRecord{
    {},
    {},
    {},
    {},
}
n, ok := runtime.GoroutineProfile(sr)
fmt.Printf("n-GoroutineProfile : %d\n", n)
if ok {
    for i, p := range sr {
        fmt.Printf("GoroutineProfile-%d: %v\n", i, p.Stack0)
    }
}
// GoroutineProfile-0: [17609957 17607648 16957534 17123361 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
// GoroutineProfile-1: [17610480 17123361 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
// GoroutineProfile-2: [17610560 17123361 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
// GoroutineProfile-3: [17610640 17123361 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]

参考文献

【Golang】Golang の map の非スレッドセーフ性と排他制御

Golang の map はスレッドセーフでない

Golang の map はスレッドセーフ、もといゴルーチンセーフではない。そのため、複数のゴルーチンからの同時アクセスによって整合性が保たれない状態になることがある。

例えば、以下のように map に対して複数のゴルーチンからの書き込みが発生すると、たまに非同期書き込み失敗のエラーが発生する。

package main

func main() {

    kvs := NewKeyValueStore()

    for i := 0; i < 10; i++ {
        go func(kvs *KeyValueStore) {
            kvs.set("key", "value")
            kvs.get("key")
        }(kvs)
    }

}

type KeyValueStore struct {
    m map[string]string
}

func NewKeyValueStore() *KeyValueStore {
    return &KeyValueStore{m: make(map[string]string)}
}

func (s *KeyValueStore) set(k, v string) {
    s.m[k] = v
}

func (s *KeyValueStore) get(k string) (string, bool) {
    v, ok := s.m[k]
    return v, ok
}

実行結果(たまに起こる)

fatal error: concurrent map writes

map がなぜデフォルトで非スレッドセーフになっているかというと、単純にパフォーマンス上不利であるためと思われる。この仕様のため、複数御ルーチンから map アクセスを実施するには明示的に排他制御を行う必要がある。1

map の排他制御

Go で排他制御を実現するには、sync.Mutex を使用する。

package main

import (
    "fmt"
    "sync"
)

func main() {

    kvs := NewConcurrentKeyValueStore()

    for i := 0; i < 10; i++ {
        go func(kvs *ConcurrentKeyValueStore) {
            kvs.set("key", "value")
            kvs.get("key")
        }(kvs)
    }

}

type ConcurrentKeyValueStore struct {
    m  map[string]string
    mu sync.RWMutex
}

func NewConcurrentKeyValueStore() *ConcurrentKeyValueStore {
    return &ConcurrentKeyValueStore{m: make(map[string]string)}
}

func (s *ConcurrentKeyValueStore) set(k, v string) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.m[k] = v
}

func (s *ConcurrentKeyValueStore) get(k string) (string, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    v, ok := s.m[k]
    return v, ok
}

上記のコードでは sync.RWMutex を使用している。sync.RWMutexsync.Mutex が提供する Lock()/Unlock() のほか、 RLock()/RUnlock() を提供する。

  • Lock()/Unlock() は占有ロック。ロック時に対象への書き込み、読み込みを禁止する。
  • RLock()/RUnlock() は共有ロック。ロック時に対象への書き込みのみ禁止し、読み込みは禁止しない。

読み込み処理の場合、他ゴルーチンからの読み込み処理も考慮して RLock を使用していくと良い。

Go の競合検出

Go ではコードの実行・ビルド時、あるいはパッケージインストール時に -race オプションを指定することで、実行中の競合検出を行うことができる。

Golang の並列実行時の競合状態検出

参考文献


  1. 例えば java も同様の理由で java.util.HashMap などは非スレッドセーフ。ただし初期の頃の動的配列などはそうではなく、同期的な java.util.Vector のみが提供されていた。これも後に非同期的な java.util.List 具象型( ArrayList など)に置き換えられた。同期的動的配列は Collections.synchronizedList(new ArrayList<>()) でラップする

【Golang】goimportsでプロジェクト配下のimport文すべてをパッケージ種類別に整形する

TL;DR

下記コマンドで、カレントディレクトリ配下のすべての .go ファイルの import 文を整形してくれる。

$ find . -print | grep --regex '.*\.go' | xargs goimports -w -local "github.com/your/package"

goimports-w オプションでファイルを直接書き換える。
また -local <string> は指定した string を prefix にもつパッケージの整形済み import 文の上部に、他の整形済み import 文が置かれる。
すなわち、string が prefix の import 文のかたまりが最後にくるようになる。

よって、import 文整形順は 標準パッケージ -> Publicパッケージ -> 自パッケージ の順となる。

あとは、find . -print | grep --regex '.*\.go' で引っ掛けた .go ファイル全てに上記 import 整形を適用する。

example

before

import (
    "fmt"
    "github.com/rennnosuke/gih/domain/model/entity"
    "github.com/rennnosuke/gih/domain/service/git/issue"
    "github.com/urfave/cli/v2"
    "regexp"
    "strconv"
    "unicode/utf8"
)

after

import (
    // 標準パッケージ
    "fmt"
    "regexp"
    "strconv"
    "unicode/utf8"

    // Publicパッケージ
    "github.com/urfave/cli/v2"

    // 自パッケージ
    "github.com/rennnosuke/gih/domain/model/entity"
    "github.com/rennnosuke/gih/domain/service/git/issue"
)

参考 : goimports

Go は標準でソースの import 文を整形するツール goimports を提供している。
goimports を Go ソースファイルに実行することで、ソースをコンパイルするのに必要なパッケージの import 文を自動挿入したり、インデントを追加したり、不要な import 文を削除してくれる。

before

package main

func main() {
    s1 := "Hello"
    s2 := "goimports."
    s := strings.join(s1, s2, ",")
    fmt.Println(s)
}
$ goimports -w main.go

after

package main

import (
    "fmt"
    "strings"
)

func main() {
    s1 := "Hello"
    s2 := "goimports."
    s := strings.Join(s1, s2, ",")
    fmt.Println(s)
}

参考文献

goimports - GoDoc

【Golang】Context

Golang における Context とは

大事なことは全部 Document に書いてある。おしまい。

context - The Go Programming Language

・・・と言ってしまうと元も子もないので、自分なりに整理してみる。

Go の Context を要約すると、

Web アプリケーションで横断的に使用するリクエストスコープ変数や処理を取り回す役割を持つもの

といえる。例えば、

が Context 内で管理される。

Go の net/http ライブラリによって起動する Web サーバーは、ひとつのリクエストにひとつのゴルーチンを割り当てて処理を開始する。リクエストを受けて実行される処理の中で、ゴルーチンは更に増えていく。Context はそのように増えていくゴルーチン間で容易に値や処理を共有できる仕組みとして提供される。

Context=文脈といった意味だが、それこそアプリケーションの文脈では「あるスコープ内で横断的に共有されるモノ」といった意味合いで使用される。Go の Context も「1 リクエスト内で共有されるデータ・処理」といった意味合いで扱われている気がする。

Context の中身

Context は以下のインタフェースによって定義されている。

type Context interface {
    Done() <-chan struct{}

    Err() error

    Deadline() (deadline time.Time, ok bool)

    Value(key interface{}) interface{}
}

Done()

キャンセルかタイムアウトを検知できるチャネルを返す。このチャネルは Context に対してキャンセルが通知されたか、タイムアウトが通知されたタイミングで閉じる。キャンセルは後述する WithCancel() WithDeadline() WithTimeout() から返る関数を呼び出すと通知できる。タイムアウトは後述する WithDeadline() WithTimeout() 関数で Context に設定できる。

Context を使う側は、この Done() から返るチャネルを経由してリクエストの状態を知ることになる。

Err()

Context がキャンセルされた理由を含む error 値が返る。
キャンセルされない間、 Err()nil を返す。

Deadline()

Context がタイムアウトになるまでの時間と、Deadline が設定されているかどうかを返す。

Value(key interface{})

Context が保持する、 keyに紐づいた値を返す。
Value() が返すべき値の管理はそれぞれの具象 Context 構造体にて定義される。


Context 単体だけではタイムアウトやキャンセルの通知はできない。Context は上記インタフェースの具象値だけでなく、後述する context パッケージ内関数と組み合わせて使う。

Context のルール

  • 保持する変数はリクエストスコープである Value() で習得できる値は、リクエストスコープ内で完結する必要がある。すなわち、複数リクエストから参照可能な値であってはならない。

  • リクエストスコープはゴルーチン安全である リクエストスコープの値は、複数ゴルーチンから同時に参照されても安全、すなわちゴルーチンセーフ(?)である必要がある。

Context の仕組み

Context Tree

Context を使う場合、単一の Context だけをずっと使い回すわけではない。Context を使うアプリ内では、最初に生成した Context から次々と子 Context を派生させ、一つの Context Tree を築いていく。

context パッケージには、 Context から子コンテキストを派生する関数が用意されている。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

func WithValue(parent Context, key, val interface{}) Context

これらの関数は引数の Context parent から子 Context を派生させる。また同時に、リクエストがキャンセルされたことを通知するための関数も返す。このキャンセル関数を呼び出すと、生成された Context とその 子 Context にキャンセルが通知される。

キャンセルが Context に通知されると、Context が持つ Done() チャネルが閉じ、 Err() が error 値を返すようになる。返す error 値は関数生成元の関数による。

「子コンテキストの派生」「キャンセルの通知関数の生成」、そして子コンテキストへの性質の付与を同時に行う理由は、アプリケーションの境界によって渡す値や伝播するイベントを制御しやすくするため。例えば、データアクセス層へ渡す Context を WithTimeout() で派生した子 Context にすることで、キャンセルイベントの伝播をデータアクセス層に閉じることができる。親 Context へはイベントは伝播しないため、上位層の Context について考えなくても良くなる。

Context を仕組みを助ける関数たち

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

引数に取る Context から新しい子 Context を生成・返戻し、加えてキャンセル関数を返す。 キャンセル関数は呼びだすと ctx とその子 Context すべてにキャンセルが通知され、各 Context の Done() チャネルが閉じ、 Err() で error 値が得られるようになる。

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

基本的には WithCancel と同じ。異なる点は、指定した d の時刻を経過したかどうかが Deadline() から得られるようになるところ。

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

基本的には WithDeadline と同じ。異なる点は、Deadline を現在時刻からの経過時間で指定する点( WithDeadline(parent, time.Now().Add(timeout)) )。

func WithValue(parent Context, key, val interface{}) Context

生成する子 Context に値を設定する関数。子 Context は新たなリクエストスコープ値を持つことになる。

Context のつかいかた

実際に Context を使ってみた。下記コードの全体はGithub 上に置いてある

Context の生成

下記コードでは、 Product 構造体配列を永続化層から取得して、標準出力へ書き出す処理を行っている。

func main() {
    r := repository.ProductRepositoryImpl{}
    s := service.ProductService{Repo: &r}

    ctx := context.Background()

    prods, err := s.GetProducts(ctx)
    if err != nil {
        panic(err)
    }

    fmt.Println(prods)
}
type Product struct {
    Name  string
    Price int
}

Context の生成は context.Background() で行う。生成した Context をアプリ内で呼び出す関数に渡していく。必要であれば、 context パッケージ上の関数で子 Context に派生したり、タイムアウトを設けたり、新たなリクエストスコープ値を作ったりする。

type ProductRepositoryImpl struct{}

// ProductService::GetProducts()が呼び出す、
// ProductRepository::GetProducts()関数の実装
func (r *ProductRepositoryImpl) GetProducts(ctx context.Context) ([]entity.Product, error) {

    c := make(chan []entity.Product, 1)

    childCtx, cancel := context.WithTimeout(ctx, time.Second*5)
    defer cancel()

    go func(ctx context.Context) { c <- r.FindProducts(ctx) }(childCtx)

    select {
    case <-childCtx.Done():
        return nil, errors.New("query canceled")
    case prods := <-c:
        return prods, nil
    }

}

上記関数では、引数で受け取った Context からタイムアウトつき子 Context を派生して使用している。この Context から得られる Done() チャネルを監視することで、キャンセルあるいはタイムアウトしたかどうかを検知することができる。

Context を運用する上で気をつけること

1. フレームワーク独自の Context との共用を避ける

フレームワークが独自に実装している Context でも標準パッケージ context とほぼ同様のことができるが、そのような Context で Wrap したりすると、フレームワークに強く依存した実装になってしまう。

2. Value は不変とする

Context が保持するリクエストスコープ値は、複数ゴルーチンから参照されても安全でなければならない

3.Context を他の構造体フィールドにしない

2. より Context の保持する値はリクエストスコープで完結させたほうがよい。Context を別の構造体のフィールドとすると、その構造体の扱い次第ではアプリケーションスコープになりえるため、特に深刻な理由がなければ構造体フィールド化を避ける。

RootContext はひとつ

リクエストスコープ変数・シグナルを管理する機構が 2 つになるため、Context Tree が 2 つ以上になる状況は厳しい。

Context は nil で渡さない

関数で渡される Context は非 nil とする。どうしても渡す Context がない時のために、そのことを明示する Context を生成する context.Todo() があるので、それを利用する。

参考文献

【Golang】APIMATIC で Postman Collection を OpenAPI Specification に変換する

PostmanはOpenAPIと互換性があり、OpenAPI Specification(以下OAS)に沿って書かれたAPIドキュメントはPostmanへimportできる。一方で、PostmanからOpenAPIへのexportはサポートされていない。これはPostmanへのFeature requestとしても挙がっており、そこそこ支持を得ている様子ではあるものの、3年前に提案されてからも未だに導入されていない。

Postmanを使ってAPIクライアントとのコミュニケーションを取りたいが、ドキュメントとして見た場合一部痒いところに手が届かない部分がある(BodyのDescription/Schemaを記述できないなど)。そのため、Postmanで記述したAPI仕様をOASに落として、管理は煩雑になるが仕様をより詳細に載せたverを作成できないかと考えた。

そこで、3rd partyから変換可能なものがないか探してみた。

Postman Collection -> OAS 変換可能な3rd partyのツール/サービス

stoplightio/api-spec-converter

Spotlight.ioでも使用されているライブラリ。Postman Collection v1のみ対応。
JSライブラリのため、変換用のコードをJSで記述する必要がある。ただ実際に変換を検証したところ、v1形式のPostman Collectionを変換しようとしたが、うまく変換できなかった。。。

また、3年前ほど前から保守がストップしている。そのため今回は使用を見送り。

APIMATIC

APIMATIC社が提供するSaaS。Postman Collection v2の変換に対応している。

アカウント登録が必要だが、結果から言うとドキュメント変換がきれいに実行できた。今回はこのツールを使用してみる。

APIMATICで Postman Collection を OASに変換する

はじめに、Postman上に定義されたAPI仕様をexportする。
exoprtしたいCollectionの三点リーダをクリックし、出現したリストの Export をクリックする。

f:id:rennnosukesann:20210102165220p:plain


f:id:rennnosukesann:20210102165247p:plain

するとexportするファイルのバージョンを選択できるので、ここではv2.1を選択。

f:id:rennnosukesann:20210102165305p:plain

これで、 Postman Collection と呼ばれる json ファイル形式のAPIドキュメントが出力される。 このファイルはPostman間の定義や設定共有に使用されるものだが、今回はその中のAPI定義部分を抽出し、OASとして変換する。

次に、 APIMANICのWebページを開き、 Sign Up for free をクリック。

f:id:rennnosukesann:20210102165311p:plain

新規アカウント登録画面に遷移するので、必要な情報を入力し、 Sign Up をクリック。

f:id:rennnosukesann:20210102165317p:plain

しばらくすると登録したメールアドレスにconfirmメールが来るので、メール内のconfirmボタンをクリックし、 Click here をクリックする。

f:id:rennnosukesann:20210102165322p:plain

APIMATICダッシュボード画面に遷移する。 Transform API をクリック。

f:id:rennnosukesann:20210102165325p:plain

先程exportした Postman Collection jsonファイルを選択し、 好きな形式を選択してexportできる。今回はOAS3.0を選択。Convertボタンをクリック。

f:id:rennnosukesann:20210102165328p:plain

最後にうまく変換できたかどうかが表示されるので、確認した上でProceedボタンを押して変換後ファイルをダウンロード。

f:id:rennnosukesann:20210102165335p:plain

完了。

以下のようなOAS3.0形式ファイルが出力される。

openapi: 3.0.0
info:
  title: test
  contact: {}
  version: '1.0'
servers:
- url: http://hoge.api.com
  variables: {}
paths:
  /test:
    get:
      tags:
      - Misc
      summary: http://hoge.api.com/test
      description: http://hoge.api.com/test
      operationId: http://hoge.api.com/test
      parameters: []
      responses:
        200:
          description: ''
          headers: {}
      deprecated: false
tags:
- name: Misc
  description: ''

これで、 OpenAPI形式への変換が完了した。

これでOASに合わせて記述を追加したり、OAS対応のサービスでおしゃれにレンダリングしたりできる。

参考文献

【Golang】Go のSSA表現形式のダンプとその解析

SSA 形式とは

静的単一代入(static simple-assignment)形式と呼ばれ、コンパイラによって生成される中間表現のこと。 SSA は、各変数への代入が一度のみ行われる式によって構成される。

例えば、以下のような式を含むコードを見ると、一行目のコードが不要であることが明らかにわかる。ただし、プログラム上で一行目を判断するためには、 Reaching-Defenitionのような分析手法とそれを実現するアルゴリズムを実行しなければならない。

y := 1
y := 2
x := y

(上記擬似コードでは := で変数の宣言・代入を表現している。重複した宣言は許可されている)

SSA 表現形式 では、変数への代入がただ一度になるようにコードが変換される。
上記のコードは、SSA 形式の表現では以下のように表現される。

y_1 := 1
y_2 := 2
x_1 := y_2

SSA では、重複する変数には各変数を別物として扱うため添字が付与される。
上記の SSA 形式表現では y_1 が参照されないことは明らかであり、プログラム上からもデッドコードとして容易に判断できる。

ちなみに、分岐内で同じ変数への代入が発生しても、SSA 形式では別の変数への代入として表現される。分岐終了後に元の変数への参照が発生した場合は、SSA 表現内では分岐によって参照する変数を切り替える phi 関数(実際には条件によって切り替わるマーカー)を参照する。

x := 1
if x > 2 {
    x := 2
} else {
    x := x + 1
}
y := x * 2
x := y
x_1 := 1
if x_1 > 2 {
    x_2 := 2
} else {
    x_3 := x_1 + 1
}
y_1 := phi(x_2, x_3) * 2
x_4 := y_1

この SSA 形式表現は、上記のデッドコードの発見・削除のようなコンパイラ最適化処理のために利用できる。
Go においても、この中間表現によるコンパイル最適化が行われている。

Go コードから SSA 形式表現を出力する

Go による SSA 形式出力は Go1.7 よりサポートされた。

  • GOSSAFUNC 環境変数を変更して go build
  • go tool compile オプション指定

のどちらかを実行すると、SSA 形式表現を記述したファイルをダンプすることができる。

GOSSAFUNC 環境変数を変更して go build する

GOSSAFUNC 環境変数にターゲットとなる関数を指定して .go ファイルをビルドすると、 .html 形式の SSA 形式表現ファイルが得られる。

$ env GOSSAFUNC=[関数名] go build main.go

この html ファイルには標準の SSA 形式表現のほか、最適化後の SSA 形式表現なども描画されている。

go tool compile オプションを指定する

go tool compile のオプション 'ssa' を指定することでも SSA 形式を出力できる。

$ go tool compile -d 'ssa/build/dump=main' main.go

ssa/help を指定することで、オプションの使用方法を確認できる。

$ go tool compile -d 'ssa/help'
compile: PhaseOptions usage:

    go tool compile -d=ssa/<phase>/<flag>[=<value>|<function_name>]

where:

- <phase> is one of:
    check, all, build, intrinsics, number_lines, early_phielim
    early_copyelim, early_deadcode, short_circuit, decompose_args
    decompose_user, opt, zero_arg_cse, opt_deadcode, generic_cse, phiopt
    nilcheckelim, prove, fuse_plain, decompose_builtin, softfloat, late_opt
    dead_auto_elim, generic_deadcode, check_bce, branchelim, fuse, dse
    writebarrier, insert_resched_checks, lower, lowered_cse
    elim_unread_autos, lowered_deadcode, checkLower, late_phielim
    late_copyelim, tighten, late_deadcode, critical, phi_tighten
    likelyadjust, layout, schedule, late_nilcheck, flagalloc, regalloc
    loop_rotate, stackframe, trim

- <flag> is one of:
    on, off, debug, mem, time, test, stats, dump

- <value> defaults to 1

- <function_name> is required for the "dump" flag, and specifies the
  name of function to dump after <phase>

Phase "all" supports flags "time", "mem", and "dump".
Phase "intrinsics" supports flags "on", "off", and "debug".

If the "dump" flag is specified, the output is written on a file named
<phase>__<function_name>_<seq>.dump; otherwise it is directed to stdout.

Examples:

    -d=ssa/check/on
enables checking after each phase

    -d=ssa/all/time
enables time reporting for all phases

    -d=ssa/prove/debug=2
sets debugging level to 2 in the prove pass

Multiple flags can be passed at once, by separating them with
commas. For example:

    -d=ssa/check/on,ssa/all/time

例えば、 'ssa/build/dump=[関数名]' を指定して、SSA 形式表現のダンプファイル( .dump )を得る。

$ go tool compile -d 'ssa/build/dump=main' main.go
$ ls
main.go              main.o               main_01__build.dump

'ssa/opt/dump=[関数名]' を指定すると、最適化後の SSA 形式表現のダンプファイル() .dump )を得られる。

Go の SSA を解析する

実際に、SSA 形式表現の Go コードをダンプしてその中身を見てみる。

hoge.go

package hoge

func hoge() int {
  x := 1
  return x
}

用意したのは素朴な関数一つを宣言した .go ファイル。
関数 hoge 内では変数 x を宣言、 整数 1 を代入し関数の返戻値としている。
この関数のダンプを取ると以下のようになる。

hoge func() int
  b1:                                   // ブロックラベル
    v1 = InitMem <mem>                  // ヒープメモリ領域の初期化。先頭アドレスがv1に格納される。
    v2 = SP <uintptr>                   // スタックポインタ(スタックの先頭アドレス)
    v3 = SB <uintptr> DEAD              // スタックベース(スタックの底アドレス。デッドコード)
    v4 = LocalAddr <*int> {~r0} v2 v1   // int型のローカルアドレス : v2上にマウントされる。{~r0}は結果を格納する変数
    v5 = Const64 <int> [0] DEAD         // int型定数 (x := 1のうち 宣言 var x; の部分。デッドコード)
    v6 = Const64 <int> [1] (x[int])     // int型定数 (x := 1のうち 代入 x = 1; の部分。)
    v7 = VarDef <mem> {~r0} v1          // 新規変数{~r0}を定義。参照先はv1
    v8 = Store <mem> {int} v4 v6 v7     // v7上のメモリでv6 を v4に格納する。該当するアドレスがv8に格納される。
    Ret v8

上記 SSA の変数のうち、 v3 v5 は使われていないためデッドコード扱いとなっている。
デッドコードを削除し、最適化された SSA 形式表現は以下のようになる。

hoge func() int
  b1:
    v1 = InitMem <mem>
    v2 = SP <uintptr>
    v4 = LocalAddr <*int> {~r0} v2 v1
    v6 = Const64 <int> [2] (x[int])
    v7 = VarDef <mem> {~r0} v1
    v8 = Store <mem> {int} v4 v6 v7
    Ret v9

表現形式こそ異なれど、処理の流れは Go バイナリと同じなのでバイナリ実行時も同様の最適化が行われている。
このように、最適化前後の SSA 形式表現を比較することでどの処理が最適化されているのか把握できる。

試しに、 hoge.go を以下のように改変してみる。

package hoge

func hoge() int {
  x := 1
  x = 2
  return x
}

明らかに、 x := 1 における代入部分が無駄なことがわかる。
SSA に起こすと以下のようになる。

hoge func() int
  b1:
    v1 = InitMem <mem>
    v2 = SP <uintptr>
    v3 = SB <uintptr> DEAD                // デッドコード
    v4 = LocalAddr <*int> {~r0} v2 v1
    v5 = Const64 <int> [0] DEAD           // x = 0 (x := 1 の宣言、デッドコード)
    v6 = Const64 <int> [1] (x[int]) DEAD  // x = 1 (x := 1 の代入、デッドコード)
    v7 = Const64 <int> [2] (x[int])       // x = 2
    v8 = VarDef <mem> {~r0} v1
    v9 = Store <mem> {int} v4 v7 v8
    Ret v9

代入 x = 2 が一つ増えていることがわかる。
また x = 2 によって x = 1 の代入がデッドコードになっている。
これを最適化すると、

hoge func() int
  b1:
    v1 = InitMem <mem>
    v2 = SP <uintptr>
    v4 = LocalAddr <*int> {~r0} v2 v1
    v7 = Const64 <int> [2] (x[int])
    v8 = VarDef <mem> {~r0} v1
    v9 = Store <mem> {int} v4 v7 v8
    Ret v9

のように変数ラベルこそ異なるものの同じ最適化結果となる。

参考文献

【Golang】Go 1.14 の重複するメソッドを持つインターフェース埋め込みの許可

proposal/6977-overlapping-interfaces.md at master · golang/proposal

Go 1.14 より、重複するメソッドを持ったインタフェースの複数埋め込みが可能になった。

インタフェースの埋め込みは、あるインタフェース A を別のインタフェース B 内で宣言することで、A で宣言したメソッドを B でも宣言したことにできる機能。

// on Package io :
// type ReadCloser interface {
//     Reader
//     Closer
// }
import io

type CustomReadCloser interface {
    io.ReadCloser
    isClosed() bool
}

Go1.13 までは、言語仕様としてインタフェースのメソッドには一意性制約があるため、B が A の宣言するメソッドを既に宣言していたり、埋め込んだインタフェース同士が同じメソッドを持つとコンパイルエラーとなっていた。

Go1.14 では「埋め込んだインタフェース同士が同じメソッドを持つ」場合に関してはコンパイルが通る様になる。

例えば、下記のインタフェース埋め込みは Go 1.13 以前は不可能だったが、1.14 より可能になる。

// on Package io :
// type ReadCloser interface {
//     Reader
//     Closer
// }
// type WriteCloser interface {
//     Writer
//     Closer
// }
import io

type ReadWriteCloser interface {
    io.ReadCloser
    io.WriteCloser
}

io.ReadCloserio.WriteCloser は共に Close() メソッドを宣言しているが、Go1.14 ではコンパイルが通る。

ただし、メソッドの名称が同一だがシグネチャが異なる場合、埋め込みは許可されない。

type E1 interface {
    m(x int) bool
}
type E2 interface {
    m(x float32) bool
}
type I  interface {
    E1
    E2
}

議論として、埋め込まれた側のインタフェースが埋め込みインタフェースと同一のメソッドを持つ場合どうするか問題が議論に上っていたが、上記の通りGo の言語仕様のインタフェース一意性の項目

Explicitly declared methods in an interface must remain unique, as before.

と書かれているため、多分許可されないままだと思われる。

参考文献