LoginSignup
3
2

More than 5 years have passed since last update.

Goのオブジェクト指向プログラミングとpackageの実装

Last updated at Posted at 2017-04-16

想定読者

  • golangを触ったことがある
  • オブジェクト指向プログラミングに興味がある

ゴール

  • golangのpackageを作成し、main packageで使用できるようになる
  • golangにおけるオブジェクト指向プログラミングの実装のイメージがつかめる

※ 今回の要点は上記の2点なので、golangについての細かな文法や、標準パッケージについては詳細に解説しない

背景

いままでいくつかの言語で、テキストファイルをパースして処理して出力するようなツールを書いて使ってきた。
そのツール作成で得られた成果物を、再利用していきたいと思い、perlやgolangでライブラリ化を試みている。
個人的にはライブラリ化とオブジェクト指向の扱いにおいてgolangのそれが好みなので、実践してみた。

さっそくできたもの

おなじみログファイルの解析をログの形式に合わせてpackageとして簡易的にライブラリ化してみた。
まずはユーザ側のソースコード(main package)から。

main.go
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がオススメ。

ちなみに処理するログファイルはこんな感じ。

log.txt
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だ。

canlog.go
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を使ったオブジェクト指向なライブラリの作成と、利用の順序は以下のような具合(一例)。

  1. packageを定義する
    • canlog.goの一行目にてpackage canlogという宣言で、mainとは異なるパッケージを定義する(他の言語におけるcalssにあたる)
      • これにより、canlogパッケージをインポートしているパッケージでは、標準ライブラリなどと同じように、canlog.hogeという具合でシンボルを参照することができる
  2. 構造体や型定義にてオブジェクトを定義する
    • canlog.goでは、Canlogというログのフィールドに対応付けた構造体と、その配列であるCanlogsを定義している
  3. オブジェクトに対応付けたメソッドやローカルな関数を定義する
    • canlog.goでは、オブジェクトのコンストラクタであるNew()や、ログを解析するParse()や、パッケージ独自のオブジェクト操作用の関数を定義している
      • なお、Goでは、大文字から始まるシンボルはパッケージ外から可能となり、小文字から始まるものは、ローカルなシンボルとして取り扱われる
  4. 他のパッケージから参照する
    • 今回はmain packageからcanlogパッケージをインポートして参照している
      • Goの環境自体にインストールしないなら、参照先(大抵はmain package)のカレントに./packagename/packagename.goという配置で、./packagenameをimportする形で参照できる(作成しているプログラム単位でのライブラリとして運用するということ)
    • 参照は、全てのシンボルにおいて、packagename.Simbolnameという文法で参照できる
    • func (records *Canlogs) PrintLog(opt int)のように、レシーバへを対称としたメソッドは、objectname.Methodnameという文法で参照できる

まとめ・TODO

  • まとめ
    • 自分の見てきた中では、オブジェクト指向の表現方法として、手続き型言語(clangなど)をルーツとするプログラマでも理解しやすく書きやすいと感じた
    • あとはなんとなく書いていて気持ちいい感じがGoらしい
  • TODO
    • Goでは、データ型を抽象化して扱えるinterface{}という型があるので、それを使いこなしてよりオブジェクト指向なGoの使いこなしをしてみようと思う
3
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2