想定読者
- golangを触ったことがある
- オブジェクト指向プログラミングに興味がある
ゴール
- golangのpackageを作成し、main packageで使用できるようになる
- golangにおけるオブジェクト指向プログラミングの実装のイメージがつかめる
※ 今回の要点は上記の2点なので、golangについての細かな文法や、標準パッケージについては詳細に解説しない
背景
いままでいくつかの言語で、テキストファイルをパースして処理して出力するようなツールを書いて使ってきた。
そのツール作成で得られた成果物を、再利用していきたいと思い、perlやgolangでライブラリ化を試みている。
個人的にはライブラリ化とオブジェクト指向の扱いにおいてgolangのそれが好みなので、実践してみた。
さっそくできたもの
おなじみログファイルの解析をログの形式に合わせてpackageとして簡易的にライブラリ化してみた。
まずはユーザ側のソースコード(main package)から。
package main
import (
"./canlog"
"log"
"os"
)
func main() {
filename := os.Args[1]
ids := []string{"1F3", "710", "1C8"}
c := canlog.New()
err := c.Parse(filename)
if err != nil {
log.Fatal(err)
}
ca := canlog.PickRecord(c, ids)
ca.PrintLog(canlog.WHOLE)
c = canlog.DelRecord(c, ids)
c.PrintLog(canlog.WHOLE)
c.PrintLog(canlog.WITHDIFFTIME)
c.PrintLog(canlog.DATA)
}
canlog.*
というメソッドを利用して、簡単に処理を連ねている。
引数の処理とかはとりあえず適当。
真面目にやるならkingpinがオススメ。
ちなみに処理するログファイルはこんな感じ。
date Thu Apr 11 04:41:25 pm 2013
base hex timestamps absolute
internal events logged
// version 8.0.0
Begin Triggerblock Thu Apr 11 04:41:25 pm 2013
0.000000 Start of measurement
0.001316 CAN 1 Status:chip status error active
0.001399 1 1F3 Rx d 3 00 10 00 Length = 146000 BitCount = 77 ID = 499
0.002763 1 1E5 Rx d 8 4C 00 21 10 00 00 00 B9 Length = 228000 BitCount = 118 ID = 485
0.003009 1 710 Rx d 8 00 5F 00 00 00 00 13 BE Length = 238000 BitCount = 123 ID = 1808
0.003175 1 C7 Rx d 4 00 38 26 9B Length = 158000 BitCount = 83 ID = 199
0.003349 1 1CC Rx d 4 00 00 00 00 Length = 165883 BitCount = 87 ID = 460
0.003586 1 F9 Rx d 8 00 DA 40 33 D0 63 FF 1C Length = 228000 BitCount = 118 ID = 249
0.003738 1 1CF Rx d 3 00 00 05 Length = 144000 BitCount = 76 ID = 463
0.003976 1 711 Rx d 8 00 23 00 7E FF EB FC 6F Length = 230000 BitCount = 119 ID = 1809
0.004148 1 1D0 Rx d 4 00 00 00 00 Length = 164000 BitCount = 86 ID = 464
0.004382 1 C1 Rx d 8 30 14 F6 08 32 B4 F7 70 Length = 226000 BitCount = 117 ID = 193
0.004615 1 C5 Rx d 8 31 27 F8 44 32 B0 F8 5C Length = 224121 BitCount = 116 ID = 197
0.004825 1 BE Rx d 6 00 00 4D 00 00 00 Length = 202242 BitCount = 105 ID = 190
0.005051 1 D1 Rx d 7 80 00 BF FE 00 FE 00 Length = 218121 BitCount = 113 ID = 209
0.005292 1 C9 Rx d 8 80 2C 5A 60 00 00 18 00 Length = 232242 BitCount = 120 ID = 201
0.005538 1 1C8 Rx d 8 80 00 00 00 FF FE 3F FE Length = 238121 BitCount = 123 ID = 456
0.005774 1 18E Rx d 8 00 00 00 84 78 46 08 45 Length = 228242 BitCount = 118 ID = 398
これをmain.go
で書いたプログラムで処理すると、色々といい感じに出力することができる。
$go run main.go log.txt
0.001399 1 1F3 Rx 3 00 10 00
0.003009 1 710 Rx 8 00 5F 00 00 00 00 13 BE
0.005538 1 1C8 Rx 8 80 00 00 00 FF FE 3F FE
0.002763 1 1E5 Rx 8 4C 00 21 10 00 00 00 B9
0.003175 1 0C7 Rx 4 00 38 26 9B
0.003349 1 1CC Rx 4 00 00 00 00
0.003586 1 0F9 Rx 8 00 DA 40 33 D0 63 FF 1C
0.003738 1 1CF Rx 3 00 00 05
0.003976 1 711 Rx 8 00 23 00 7E FF EB FC 6F
0.004148 1 1D0 Rx 4 00 00 00 00
0.004382 1 0C1 Rx 8 30 14 F6 08 32 B4 F7 70
0.004615 1 0C5 Rx 8 31 27 F8 44 32 B0 F8 5C
0.004825 1 0BE Rx 6 00 00 4D 00 00 00
0.005051 1 0D1 Rx 7 80 00 BF FE 00 FE 00
0.005292 1 0C9 Rx 8 80 2C 5A 60 00 00 18 00
0.005774 1 18E Rx 8 00 00 00 84 78 46 08 45
0.001364 0.002763 1 1E5 Rx 8 4C 00 21 10 00 00 00 B9
0.000166 0.003175 1 0C7 Rx 4 00 38 26 9B
0.000174 0.003349 1 1CC Rx 4 00 00 00 00
0.000237 0.003586 1 0F9 Rx 8 00 DA 40 33 D0 63 FF 1C
0.000152 0.003738 1 1CF Rx 3 00 00 05
0.000238 0.003976 1 711 Rx 8 00 23 00 7E FF EB FC 6F
0.000172 0.004148 1 1D0 Rx 4 00 00 00 00
0.000234 0.004382 1 0C1 Rx 8 30 14 F6 08 32 B4 F7 70
0.000233 0.004615 1 0C5 Rx 8 31 27 F8 44 32 B0 F8 5C
0.000210 0.004825 1 0BE Rx 6 00 00 4D 00 00 00
0.000226 0.005051 1 0D1 Rx 7 80 00 BF FE 00 FE 00
0.000241 0.005292 1 0C9 Rx 8 80 2C 5A 60 00 00 18 00
0.000236 0.005774 1 18E Rx 8 00 00 00 84 78 46 08 45
4C002110000000B9
0038269B
00000000
00DA4033D063FF1C
000005
0023007EFFEBFC6F
00000000
3014F60832B4F770
3127F84432B0F85C
00004D000000
8000BFFE00FE00
802C5A6000001800
0000008478460845
さて、main.go
ではimport
で./canlog
をインポートしているが、その中身がこのcanlog packageだ。
package canlog
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
const (
WHOLE = iota
WITHDIFFTIME
DATA
)
// Canlog型はCanlogに合わせたデータ構造を持ったオブジェクト
type Canlog struct {
Prevtime float64
Crnttime float64
Difftime float64
Ch string
Id string
Dir string
Stat string
Dlc int
Data []int64
Remain string
}
type Canlogs []Canlog
// Newは、新たにCanlogオブジェクトを作成する
func New() Canlogs {
var c Canlogs
return c
}
// Parseは、ファイルを解析してCanlogオブジェクトを作成する
func (records *Canlogs) Parse(filename string) error {
fp, err := os.Open(filename)
if err != nil {
return err
}
defer fp.Close()
scanner := bufio.NewScanner(fp)
p := 0.000000
for scanner.Scan() {
c := new(Canlog)
fs := strings.Fields(scanner.Text())
if fs[1] == "1" || fs[1] == "2" {
c.Prevtime = p
c.Crnttime, _ = strconv.ParseFloat(fs[0], 32)
c.Difftime = c.Crnttime - p
c.Ch = fs[1]
c.Id = fs[2]
c.Dir = fs[3]
c.Stat = fs[4]
c.Dlc, _ = strconv.Atoi(fs[5])
for _, f := range fs[6 : c.Dlc+6] {
d, _ := strconv.ParseInt(f, 16, 32)
c.Data = append(c.Data, d)
}
c.Remain = strings.Join(fs[c.Dlc+7:c.Dlc+15], " ")
*records = append(*records, *c)
p = c.Crnttime
}
}
return scanner.Err()
}
// PickRecordは、引数で渡したCanlogオブジェクトからidsで渡したIDが存在するレコードのみを抽出したCanlogオブジェクトを返す
func PickRecord(records Canlogs, ids []string) Canlogs {
var nc Canlogs
for _, r := range records {
if isContains(r.Id, ids) {
nc = append(nc, r)
}
}
return nc
}
// DelRecordは、引数で渡したCanlogオブジェクトからidsで渡したIDが存在するレコードを削除したCanlogオブジェクトを返す
func DelRecord(records Canlogs, ids []string) Canlogs {
var nc Canlogs
for _, r := range records {
if !isContains(r.Id, ids) {
nc = append(nc, r)
}
}
return nc
}
// isContainsは、文字列tgtが、配列arrに含まれているかをbooleanとして返す
func isContains(tgt string, arr []string) bool {
for _, e := range arr {
if e == tgt {
return true
}
}
return false
}
// PrintLogは、Canlogsオブジェクトを1レコードずつフォーマットして標準出力へ出力する
func (records *Canlogs) PrintLog(opt int) {
for _, r := range *records {
switch opt {
case WHOLE:
s := convertDataToString(r, " ")
fmt.Printf("%06f %s %03s %s %X %s\n", r.Crnttime, r.Ch, r.Id, r.Dir, r.Dlc, s)
case WITHDIFFTIME:
s := convertDataToString(r, " ")
fmt.Printf("%06f %06f %s %03s %s %X %s\n", r.Difftime, r.Crnttime, r.Ch, r.Id, r.Dir, r.Dlc, s)
case DATA:
s := convertDataToString(r, "")
fmt.Printf("%s\n", s)
default:
s := convertDataToString(r, " ")
fmt.Printf("%06f %s %03s %s %X %s\n", r.Crnttime, r.Ch, r.Id, r.Dir, r.Dlc, s)
}
}
}
// convertDataToStringは、レコードの配列データから、sepで区切った文字列へ変換する
func convertDataToString(record Canlog, sep string) string {
var data []string
for _, d := range record.Data {
data = append(data, fmt.Sprintf("%02X", d))
}
return strings.Join(data, sep)
}
packageを使ったオブジェクト指向なライブラリの作成と、利用の順序は以下のような具合(一例)。
- packageを定義する
-
canlog.go
の一行目にてpackage canlog
という宣言で、mainとは異なるパッケージを定義する(他の言語におけるcalssにあたる)- これにより、
canlog
パッケージをインポートしているパッケージでは、標準ライブラリなどと同じように、canlog.hoge
という具合でシンボルを参照することができる
- これにより、
-
- 構造体や型定義にてオブジェクトを定義する
-
canlog.go
では、Canlog
というログのフィールドに対応付けた構造体と、その配列であるCanlogs
を定義している
-
- オブジェクトに対応付けたメソッドやローカルな関数を定義する
-
canlog.go
では、オブジェクトのコンストラクタであるNew()
や、ログを解析するParse()
や、パッケージ独自のオブジェクト操作用の関数を定義している- なお、Goでは、大文字から始まるシンボルはパッケージ外から可能となり、小文字から始まるものは、ローカルなシンボルとして取り扱われる
-
- 他のパッケージから参照する
- 今回はmain packageから
canlog
パッケージをインポートして参照している- Goの環境自体にインストールしないなら、参照先(大抵はmain package)のカレントに
./packagename/packagename.go
という配置で、./packagename
をimportする形で参照できる(作成しているプログラム単位でのライブラリとして運用するということ)
- Goの環境自体にインストールしないなら、参照先(大抵はmain package)のカレントに
- 参照は、全てのシンボルにおいて、
packagename.Simbolname
という文法で参照できる -
func (records *Canlogs) PrintLog(opt int)
のように、レシーバへを対称としたメソッドは、objectname.Methodname
という文法で参照できる
- 今回はmain packageから
まとめ・TODO
- まとめ
- 自分の見てきた中では、オブジェクト指向の表現方法として、手続き型言語(clangなど)をルーツとするプログラマでも理解しやすく書きやすいと感じた
- あとはなんとなく書いていて気持ちいい感じがGoらしい
- TODO
- Goでは、データ型を抽象化して扱える
interface{}
という型があるので、それを使いこなしてよりオブジェクト指向なGoの使いこなしをしてみようと思う
- Goでは、データ型を抽象化して扱える