Goで
かんたん
ソースコードの
静的解析
2017/02/10(土)
@プロ生勉強会 第46回
The Go gopher was designed by Renee French.
The gopher stickers was made by Takuya Ueda.
Licensed under the Creative Commons 3.0
Attributions license.
自己紹介
メルカリ/ソウゾウ
上田拓也
twitter: @tenntenn
■ コミュニティ活動
Google Cloud Platform User Group (GCPUG) Tokyo
Goビギナーズ
golang.tokyo
Go Conference
■ 業務
GAE/Goでメルカリアッテを作ってます
GoやGCPコミュニティを盛り上げる仕事
Gopherを描く仕事(LINEスタンプ)
2
アジェンダ
■ Goについて知ろう
● Goの特徴
● Goの基本的な文法
● 並行プログラミング
● 標準パッケージ
● 開発ツールと開発環境
● シングルバイナリ/クロスコンパイル
■ 開発ツールとソースコードの静的解析
● ソースコードの静的解析について
● 開発ツールと静的解析
● goパッケージついて
● 静的解析のきほん
3
Goについて知ろう
4
Goとは?
Googleが開発しているプログラミング言語
■ 特徴
● 強力でシンプルな言語設計と文法
● 並行プログラミング
● 豊富な標準ライブラリ群
● 周辺ツールの充実
● シングルバイナリ/クロスコンパイル
5
GoのSimpleとEasyの実現
■ Simple:言語自体
● 文法や言語思想
● ゴルーチンとチャネル
■ Easy:ライブラリや開発環境
● シングルバイナリ/クロスコンパイル
● 豊富な標準パッケージ
● 開発ツール充実
6
各言語の構成要素が
直交するように
強力でシンプルな言語設計と文法
■ スクリプト言語の書きやすさ
● 冗長な記述は必要ない
■ 型のある言語の厳密さ
● 曖昧な記述はできない
■ 考えられたシンプルさ
● 機能を増やすことで言語を拡張していくこと
はしない
7
Hello, Worldを見てみる
8
package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
他の言語と
そう変わらない!
実行可能なバイナリを作るには、
mainパッケージにする必要がある
mainパッケージのmain関数が
プログラムのエントリポイント
変数の定義と代入
■ 以下はすべて同じ意味
9
var n int
n = 100
var n int = 100
n := 100
int型の変数を宣言
nに100を入れる
型は後ろに書く!
宣言と代入を同時にする
:=を使って型推論を行い
宣言と代入を行う
異なる型の演算
■ 型が異なると演算できない
10
var n int = 100
var m float64 = 0.5
sum := n + m
コンパイルエラーになる
nとmで型が違うので
演算できない
関数定義と呼び出し
■ 関数の定義
■ 関数呼び出し
11
func div(n int, m int) (int, int) {
return n / m, n % m
}
戻り値n, m int
とも書ける
多値が返せる
q, r := div(5, 3)
多値を受け取る
型は後ろに書く!
繰り返し:for
■ Goの繰り返しはforのみ
12
// いつものfor
for i := 0; i <= 100; i++ {
}
// while的な使い方
for i <= 100 {
}
// 無限ループ
for {
}
()はいらない
分岐:if
■ 条件式の前に代入文などが書ける
13
// いつものif
if a == 0 {
}
// 代入文を書く
if a := f(); a > 0 {
fmt.Println(a)
} else {
fmt.Println(2*a)
}
()はいらない
分岐:switch
■ caseに式が書ける
■ breakは書かなくてよい
14
switch a {
case 1:
fmt.Println("a is 1")
default:
fmt.Println("default")
}
swtich {
case a == 1:
fmt.Println("a is 1")
}
caseをまたぐ際には、
fallthroughを使う
何もしないと
breakになる
ユーザ型の定義
type <型名> <型リテラル>|<既存の型>
15
// 組み込み型を別の型として定義
type Int int
// 他のパッケージを別の型として定義
type MyWriter io.Writer
// 型リテラルに名前をつける
type Person struct {
Name string
}
intとIntは別の型として扱われる
メソッド
type で定義した型はメソッドのレシーバにできる
16
type Hex int
func (h Hex) String() string {
return fmt.Sprintf("%x", int(h))
}
// 100をHex型として代入
var hex Hex = 100
// Stringメソッドを呼び出す
fmt.Println(hex.String())
Playgroundで動かす
パッケージ
■ パッケージの定義とエクスポート
■ 外部パッケージの利用
17
package hoge
func MyFunc() {
}
ファイルの先頭に書く
パッケージ外から参照するには、
識別子名の先頭を大文字にする
import "hoge"
func main() {
hoge.MyFunc()
}
「パッケージ.識別子」でアクセス
importして利用する
Goの特徴
● 強力でシンプルな言語設計と文法
● 並行プログラミング
● 豊富な標準ライブラリ群
● 周辺ツールの充実
● シングルバイナリ/クロスコンパイル
18
並行プログラミング
■ ゴールーチン
● 軽量なスレッドに近いもの
● goキーワードをつけて関数呼び出し
■ チャネル
● ゴールーチン間のデータのやり取り
● 安全にデータをやり取りできる
19
チャネル
ゴールーチン
A
ゴールーチン
B
データ
データ
go f()
Goの特徴
● 強力でシンプルな言語設計と文法
● 並行プログラミング
● 豊富な標準ライブラリ群
● 周辺ツールの充実
● シングルバイナリ/クロスコンパイル
20
豊富な標準パッケージ
21
■ 標準ライブラリ一覧
 https://golang.org/pkg/
net/http HTTPサーバなど
archive, compress zipやgzipなど
flag プログラム引数
encoding JSON, XML, CSVなど
html/template HTMLテンプレート
os, path/filepath ファイル操作など
Hello, net/http パッケージ
■ 簡単にHTTPサーバが書ける
package main
import "fmt"
import "net/http"
func handler(w http.ResponseWriter,
r *http.Request) {
fmt.Fprint(w, "Hello, net/http!")
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
22
レスポンスを書き込むWriter
リクエスト
レスポンスの書き込み
ハンドラの登録
flagパッケージ
■ 簡単にコマンドライン引数を扱える
23
var msg string
func init() {
flag.StringVar(&msg,"m","hello","message")
}
func main() {
flag.Parse()
fmt.Println(msg)
}
$ ./main -m hi
hi
ファイル操作とテキスト処理
■ 簡単にコマンドライン引数を扱える
24
f, _ := os.Open("my.txt")
defer f.Close()
s := bufio.NewScanner(f)
for i := 1; s.Scan(); i++ {
fmt.Println(i, strings.TrimSpace(s.Text()))
}
ファイルを開く
$ echo -e " hello n world" > my.txt
$ ./main
1 hello
2 world
Playgroundで動かす
ファイルを読み込む
左右の空白を取り除く
Goの特徴
● 強力でシンプルな言語設計と文法
● 並行プログラミング
● 豊富な標準ライブラリ群
● 周辺ツールの充実
● シングルバイナリ/クロスコンパイル
25
開発ツールの充実1
■ 開発ツールが充実している
● go build
○ ビルドを行うコマンド
● go test
○ xxxx_test.goに書かれたテストコードの実行
● go doc/godoc
○ ドキュメント生成
● gofmt/goimports
○ コードフォーマッター
● go vet/golint
○ コードチェッカー、リンター
26
開発ツールの充実2
■ 開発ツールが充実している
● guru
○ 静的解析
○ 定義位置の検索など
● gocode
○ コード補完
● errcheck
○ エラー処理のチェック
● gorename/gomvpkg
○ リファクタリングツール
27
Goのエディタ・IDE事情
■ 決まったものはない
● Vim
● Emacs
● IntelliJ / Gogland
● VSCode
● Atom
28
好きなエディタ/IDEを
使うことができる!
エディタ間に差はないの?
開発ツールとエディタ/IDEの関係
29
エディタ/IDE
プラグイン
開発ツール
ここが同じなので、
あまり差分が出ない
Goの特徴
● 強力でシンプルな言語設計と文法
● 並行プログラミング
● 豊富な標準ライブラリ群
● 周辺ツールの充実
● シングルバイナリ/クロスコンパイル
30
シングルバイナリ/クロスコンパイル
■ 環境変数のGOOSとGOARCHを指定する
開発環境とは違うOSやアーキテクチャ向けに
クロスコンパイルできる
31
# Windows(32ビット)向けにコンパイル
$ GOOS=windows GOARCH=386 go build
# Linux(64ビット)向けにコンパイル
$ GOOS=linux GOARCH=amd64 go build
シングルバイナリになるので
動作環境を用意しなくてよい
go buildはコンパイルするコマンド
Androidで上でサーバを動かす
32
Youtubeで見る コード
母艦のシェル
adb shell
端末
Goの特徴のおさらい
● 強力でシンプルな言語設計と文法
● 並行プログラミング
● 豊富な標準ライブラリ群
● 周辺ツールの充実
● シングルバイナリ/クロスコンパイル
33
Goを勉強するには
■ golang.org
● 公式ドキュメント類が充実
■ コミュニティ
● Gophers Slack #japan
● Google+ Golang JP
■ Qiita
● Goタグでまとまっている
● Go言語の初心者が見ると幸せになれる場所
■ 書籍
● プログラミング言語Go
● みんなのGo言語
34
開発ツールと
ソースコードの静的解析
35
ソースコードの静的解析とは?
36
■ ソースコードを実行せずに解析すること
● ソースコードから抽象構文木(AST)などを取
得して解析する
● 静的型付け言語だと、静的解析で型情報が
取得できる
● 逆は実行して解析する動的解析
Goで静的解析をすると何が嬉しいのか?
● リファクタリングツール
○ 変数の宣言位置や使用箇所の抽出
○ パッケージの解析
● コードジェネレーター
○ コメントによるアノテーションの抽出
○ コードフォーマッタ
● 処理系
○ 抽象構文木の解析
○ 定数の扱い
37
静的型付け言語なので
静的解析でも多くの事が知れる
開発ツールとソースコードの静的解析
38
■ 開発ツールの多くは静的解析を行っている
● gofmt/goimports
○ コードフォーマッター
● go vet/golint
○ コードチェッカー、リンター
● guru
○ 静的解析
● gocode
○ コード補完
● errcheck
○ エラー処理のチェック
● gorename/gomvpkg
○ リファクタリングツール
■ 標準パッケージで静的解析の機能を提供
goパッケージ
39
go/ast 抽象構文木(AST)を提供
go/build パッケージに関する情報を集める
go/constant 定数に関する型を提供
go/doc ドキュメントをASTから取り出す
go/format コードフォーマッタの機能を提供
go/importer コンパイラに適したImporterを提供
go/parser 構文解析の機能を提供
go/printer ASTの表示機能を提供
go/scanner 字句解析の機能を提供
go/token トークンに関する型を提供
go/types 型チェックに関する機能を提供
静的解析の流れ
40
ソースコード
トークン
抽象構文木(AST)
型情報
構文解析
字句解析
型チェック
go/scanner
go/token
go/parser
go/ast
go/types
go/constant
字句解析 - go/scanner,go/token
■ 文字列をトークンにしていく
● 空白などを取り除き、意味のある単位=トー
クンにしていく作業
41
v + 1
IDENT ADD INT
トークン
ソースコード
構文解析 - go/parser,go/ast
■ トークンを抽象構文木(AST)にしていく
● プログラムの構造を持たせる
42
v + 1
IDENT ADD INT
ソースコード
+
v 1
BinaryExpr
Ident BasicLit
トークン
抽象構文木(AST)
型チェック - go/types,go/constant
■ 型チェックを行う
● 識別子の解決
● 型の推論
● 定数の評価
43
n := 100 + 200
fmt.Println(n)
定数の評価
=300
型の推論
-> int
識別子の解決
識別子の解決
-> fmtパッケージ
goパッケージの利用例
■ gofmt:フォーマッタ
● go/parser, go/ast
○ 抽象構文木(AST)を取得する
● go/printer
○ フォーマットして出力する
■ gorename:リネームツール
● go/parser, go/ast
○ 抽象構文木(AST)を取得する
● go/types
○ 識別子の出現場所などを取得する
■ 同じ識別子をすべてリネームする
44
自分で開発ツールを作ろう
■ Goは開発ツールが作りやすい
● シングルバイナリ
○ 配布のしやすさ
● クロスコンパイル
○ OSに依存しないツールの作成
● 豊富な標準パッケージ
○ コマンドライン引数:flag
○ ネットワーク:net
○ ファイル操作:os,ioutil
○ テキスト処理:bufio,strings
○ ソースコード解析:go
45
静的解析の流れ
46
ソースコード
トークン
抽象構文木(AST)
型情報
構文解析
字句解析
型チェック
go/scanner
go/token
go/parser
go/ast
go/types
go/constant
抽象構文木の取得
■ go/parserパッケージの関数を使う
● ParseExpr,ParseExprFrom
○ 式をパースする
○ ParseExprはParseExprFromを簡易版
● ParseFile
○ ファイル単位でパースする
● ParseDir
○ ディレクトリ単位でパースする
○ 中でParseFileを呼んでいる
47
式の抽象構文木を取得する
■ 式を構文解析する
■ ParseExprFromでも書ける
48
expr, err := parser.ParseExpr(`v + 1`)
if err != nil {
/* エラー処理 */
}
/* exprを解析する処理 */
fset := token.NewFileSet() // ファイル情報
src := []byte(`v + 1`)
f := "" // ファイル名(式なので不要)
m := 0 // モード(式なので不要)
expr, err := parser.ParseExprFrom(fset, f, s, m)
token.FileSetとは?
■ ファイル中の位置情報を記録する為の型
● 位置情報は数値で表される
● 複数のファイル間で一意の値
● 各ファイルのoffsetが記録されている
● パースする際に記録されていく
49
token.FileSetは出力引数として
Parse系の関数に渡す
ファイルからASTを取得する
■ 完全なGoのソースコードを構文解析する
50
const src = `
package main
var v = 100
func main() {
fmt.Println(v+1)
}`
fs := token.NewFileSet()
f, err := parser.ParseFile(fs, "my.go", src, 0)
if err != nil {
/* エラー処理 */
}
/* f を解析する処理
引数はparse.ExprFromと
同じ構成
srcがnilだとファイル名
でファイルを開く
解析するファイルの中身
Hello, WorldのASTの構成
51
package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
ast.File
ast.File
ast.GenDecl
ast.FuncDecl
ast.CallExpr
Goの抽象構文木(AST)を手入力してHello, Worldを作る
http://qiita.com/tenntenn/items/0cbc6f1f00dc579fcd8c
Playgroundで動かす
ASTをトラバースする
■ ast.Inspectを使う
52
n, _ := parser.ParseExpr(`v + 1`)
ast.Inspect(n, func(n ast.Node) bool {
if n != nil { fmt.Printf("%Tn", n) }
return true
})
printer.Fprint(os.Stdout, token.NewFileSet(), n)
*ast.BinaryExpr
*ast.Ident
*ast.BasicLit
v + 1
+
v 1
構文解析
抽象構文木(AST)を探索
抽象構文木(AST)を出力
BinaryExpr
Ident BasicLit
Playgroundで動かす
ast.Walkというのもある
ASTをトラバースする
■ 再帰を使ってトラバースする
53
func traverse(n ast.Node) {
switch n := n.(type) {
case *ast.Indent:
fmt.Println(n.Name)
case *ast.BinaryExpr:
traverse(n.X)
traverse(n.Y)
case *ast.UnaryExpr:
traverse(n.X)
}
}
識別子の場合は名前を出力
二項演算式の場合は
各項を探索
単項演算式の場合は
項を探索
型でswitchする
https://play.golang.org/p/5SOdiy420p
自作ツールを作ってみよう
■ goパッケージで簡単に静的解析して世界を広げよう
● コードジェネレータ
○ ASTを取得する方法を調べる
○ 抽象構文木(AST)をトラバースする
○ 抽象構文木(AST)をいじってフォーマットをかける
○ Goの抽象構文木(AST)を手入力してHello, Worldを作る
○ go-app-builderのソースコードを読む
● リファクタリングツール
○ gorenameをライブラリとして使う
○ Goのスコープについて考えてみよう
○ go/typesパッケージを使い変数名をリネームしてみる
● 処理系
○ 簡単な式の評価機を作ってみる
○ 【実践goパッケージ】文字列から複素数型の値をパースする
○ もっと楽して式の評価器を作る
54
実際に作ってみたツール
■ Structタグを揃える
■ 汎用的なCMS
● 式の評価を使ったツール
● コンテンツの配信条件を式で書く
55
type User struct {
ID int64 `json:"id" datastore:"ID"`
Name int64 `json:"name" datastore:"Name"`
}
type User struct {
ID int64 `json:"id" datastore:"ID"`
Name int64 `json:"name" datastore:"Name"`
}
まとめ
■ Goの特徴
● 強力でシンプルな言語設計と文法
● 並行プログラミング
● 豊富な標準ライブラリ群
● 周辺ツールの充実
● シングルバイナリ/クロスコンパイル
■ Goで静的解析はかんたん
● goパッケージが標準である
○ go/scanner,go/token
○ go/parser,go/ast
○ go/types,go/constant
56
Q&A
57
Thank you!
twitter: @tenntenn
Qiita: tenntenn
connpass: tenntenn
58

Goでかんたんソースコードの静的解析