M.Hiroi's Home Page

Go Language Programming

お気楽 Go 言語プログラミング入門

[ PrevPage | Golang | NextPage ]

インターフェース

今回は「インターフェース (interface)」について説明します。Go 言語のインターフェースは Java のインターフェースと同じように、メソッドの型だけを列挙した型です。Go 言語の場合、あるインターフェース Foo で宣言されているメソッドがすべて実装されている構造体であれば何でも型 Foo として扱うことができます。インターフェースを使うと、Java と同じように「ポリモーフィズム」を実現することができます。

●インターフェースの定義

それでは、具体的にインターフェースの使い方を説明しましょう。インターフェースの定義は interface 文で行います。interface の構文を示します。

type 型名 interface {
    メソッド名1(引数の型, ...) (返り値の型, ...)
      .....
    メソッド名n(引数の型, ...) (返り値の型, ...)
}

type の後ろに型名を、その後ろに interface を書き、{ } の中にメソッドの型を記述します。このとき、メソッド名の前にレシーバの型を書いてはいけません。レシーバ以外の引数の型と返り値の型を宣言してください。

簡単な例を示しましょう。点を表す型 Point をインターフェースとして定義します。

リスト : インターフェースの使用例

// Point 型
type Point interface {
    distance0() float64
}

distance0 は原点からの距離を求めるメソッドとします。構造体にこのメソッドを実装すれば、それは Point 型として扱うことができます。

次は、二次元と三次元の点を表す構造体 Point2d と Point3d を定義します。

リスト : Point2d と Point3d の定義

// 二次元
type Point2d struct {
    x, y float64
}

// Point2d の生成
func newPoint2d(x, y float64) *Point2d {
    p := new(Point2d)
    p.x, p.y = x, y
    return p
}

// メソッド
func (p *Point2d) distance0() float64 {
    return math.Sqrt(p.x * p.x + p.y * p.y)
}

// 三次元
type Point3d struct {
    x, y, z float64
}

// Point3d の生成
func newPoint3d(x, y, z float64) *Point3d {
    p := new(Point3d)
    p.x, p.y, p.z = x, y, z
    return p
}

// メソッド
func (p *Point3d) distance0() float64 {
    return math.Sqrt(p.x * p.x + p.y * p.y + p.z * p.z)
}

メソッド distance0 を定義することで、Point2d と Point3d は Point 型として扱うことができます。レシーバの型はポインタ (*Point2d, *Point3d) にしました。

それでは、実行してみましょう。

リスト : 簡単な実行例 (sample91.go)

// 合計値を求める
func sumOfDistance0(ary []Point) float64 {
    sum := 0.0
    for _, p := range ary {
        sum += p.distance0()
    }
    return sum
}

func main() {
    a := []Point{
        newPoint2d(0, 0), newPoint2d(10, 10),
        newPoint3d(0, 0, 0), newPoint3d(10, 10, 10),
    }
    fmt.Println(a[0].distance0())
    fmt.Println(a[1].distance0())
    fmt.Println(a[2].distance0())
    fmt.Println(a[3].distance0())
    fmt.Println(sumOfDistance0(a))
}
$ go run sample91.go
0
14.142135623730951
0
17.320508075688775
31.462643699419726

関数 sumOfDistance0 はスライス ary に格納されている点の距離の合計値を求めます。ary の型は [ ]Point で、Point 型であれば Point2d, Point3d でもポインタ型 (*Point2d, *Point3d) でも格納することができます。

●ポリモーフィズム

一般的なオブジェクト指向言語の場合、クラスが異なっていれば同じ名前のメソッドを定義することができます。たとえば、クラス Foo1 にメソッド bar が定義されていても、クラス Foo2 に同名のメソッド bar を定義することができます。そして、あるオブジェクトに対してメソッド bar を呼び出した場合、それが Foo1 から作られたオブジェクトであれば、Foo1 で定義された bar が実行され、Foo2 から作られたオブジェクトであれば、Foo2 で定義された bar が実行されます。

Go 言語は静的な型付けを行う言語なので、コンパイルの時点で呼び出すメソッドを可能な限り決定します。たとえば、変数 p を *Point2d、変数 q を *Point3d と宣言して、p.distance0(), q.distance0() を呼び出します。この場合、ドットの左辺の型がわかるので、コンパイル時に呼び出すメソッドを決めることが可能です。

これに対し、sumOfDistance0 の引数の型は [ ]Point なので、コンパイル時に要素の型が Point2d なのか Point3d なのか決定することはできません。したがって、プログラムの実行時に要素の型を調べて、それに関連付けられたメソッドを呼び出すことになります。このように、プログラムの実行時に呼び出すメソッドを選択する機能を「ポリモーフィズム (polymorphism)」と呼びます。

静的な型付けを行うオブジェクト指向言語、たとえば Java は継承またはインターフェースを使ってポリモーフィズムを働かせます。Go 言語の場合、構造体の埋め込みではポリモーフィズムを働かせることはできません。ポリモーフィズムを機能させる仕組みはインターフェースだけになります。

もうひとつ簡単な例として、三角形、四角形、円を操作するインターフェースを考えてみましょう。最初にインターフェースを定義します。

リスト : 図形のインターフェース

type Figure interface {
    kindOf() string
    area()   float64
    print()
}

型名は Figure にしました。kindOf は図形の種別を文字列で返すメソッドです。area は面積を求めます。print は種別と面積を表示します。これら 3 つのメソッドが備わっている構造体は型 Figure として扱うことができます。

次は、三角形 (Triangle)、四角形 (Rectangle) 、円 (Circle) の構造体とメソッドを定義します。

リスト : 図形の定義

// 三角形
type Triangle struct {
    altitude, base float64
}

func newTriangle(a, b float64) *Triangle {
    p := new(Triangle)
    p.altitude, p.base = a, b
    return p
}

func (_ *Triangle) kindOf() string { return "Triangle" }

func (p *Triangle) area() float64 {
    return p.altitude * p.base / 2
}

func (p *Triangle) print() {
    fmt.Println("Triangle : area =", p.area())
}

// 四角形
type Rectangle struct {
    width, height float64
}

func newRectangle(w, h float64) *Rectangle {
    p := new(Rectangle)
    p.width, p.height = w, h
    return p
}

func (_ *Rectangle) kindOf() string { return "Rectangle" }

func (p *Rectangle) area() float64 {
    return p.width * p.height
}

func (p *Rectangle) print() {
    fmt.Println("Rectangle: area =", p.area())
}

// 円
type Circle struct {
    radius float64
}

func newCircle(r float64) *Circle {
    p := new(Circle)
    p.radius = r
    return p
}

func (_ *Circle) kindOf() string { return "Circle" }

func (p *Circle) area() float64 {
    return p.radius * p.radius * math.Pi
}

func (p *Circle) print() {
    fmt.Println("Circle: area =", p.area())
}

レシーバの仮引数を使用しないのであれば、引数名を指定せずに匿名変数 ( _ ) で済ますことができます。あとは特に難しいところはないでしょう。

簡単な実行例を示します。

リスト : 簡単な実行例 (sample92.go)

package main

import (
    "fmt"
    "math"
)

//
// 図形の定義は省略
//

// 面積の合計値を求める
func sumOfArea(a []Figure) float64 {
    sum := 0.0
    for _, fig := range a {
        sum += fig.area()
    }
    return sum
}

func main() {
    var a Figure = newTriangle(10, 10)
    fmt.Println(a.kindOf())
    fmt.Println(a.area())
    a.print()
    a = newRectangle(10, 10)
    fmt.Println(a.kindOf())
    fmt.Println(a.area())
    a.print()
    a = newCircle(10)
    fmt.Println(a.kindOf())
    fmt.Println(a.area())
    a.print()

    var b []Figure = []Figure{
        newTriangle(100, 100), newRectangle(100, 100), newCircle(100),
    }
    fmt.Println(sumOfArea(b))
}
$ go run sample92.go
Triangle
50
Triangle : area = 50
Rectangle
100
Rectangle: area = 100
Circle
314.1592653589793
Circle: area = 314.1592653589793
46415.92653589793

面積の合計を求める関数 sumOfArea() は引数に Figure 型のスライスを受け取ります。そして、Figure に定義されているメソッドを使って図形を操作することができます。sumOfArea はメソッド area を呼び出していますが、ポリモーフィズムの働きにより構造体のメソッド area() が呼び出されるので図形の面積を正しく求めることができます。

●空インタフェースと型アサーション

Go 言語のインターフェースには「空インターフェース」という型があります。

interface{}

文字通りメソッドの定義がないインターフェースです。Go 言語の場合、interface{ } で宣言された変数や配列は、Go 言語の型であれば何でも格納することができます。そして、「型アサーション (type assertion)」という機能を使うと、インターフェース型の値から元の型の値を求めることができます。

型アサーションの構文を示します。

value, ok := x.(T)

x はインターフェース型の変数で、その後ろにドット ( . ) とカッコを記述します。カッコの中には型を指定します。x の値が型 T であれば、value にはその値がセットされ、ok の値は true になります。型 T ではない場合、value には型 T のゼロ値がセットされ、ok の値は False になります。また、value = x.(T) で元の値だけ受け取ることもできます。この場合、型変換に失敗するとランタイムエラーが発生します。

簡単な例を示しましょう。次のリストを見てください。

リスト : 空インターフェースと型アサーション (sample93.go)

package main

import "fmt"

// 整数の合計値を求める
func sumOfInt(ary []interface{}) int {
    sum := 0
    for _, x := range ary {
        v, ok := x.(int)
        if ok { sum += v }
    }
    return sum
}

// 実数の合計値を求める
func sumOfFloat(ary []interface{}) float64 {
    sum := 0.0
    for _, x := range ary {
        v, ok := x.(float64)
        if ok { sum += v }
    }
    return sum
}

func main() {
    a := []interface{}{1, 1.1, "abc", 2, 2.2, "def", 3, 3.3}
    fmt.Println(sumOfInt(a))
    fmt.Println(sumOfFloat(a))
}
$ go run sample93.go
6
6.6

関数 sumOfInt は int の合計値を求め、sumOfFlaot は float64 の合計値を求めます。どちらの関数も型アサーションで型をチェックして、正しい型であれば sum に値 v を加算します。main 関数のスライス a は [ ]interface{ } で宣言します。int や float64 だけではなく文字列も格納することができます。

●型 switch

データの型判定は switch 文でも行うことができます。Go 言語では、これを「型 switch 」といいます。型 switch の構文を示します。

switch v := x.(type) {
case 型1: ...  // v は型1 の値になる
case 型2: ...  // v は型2 の値になる
    ...
default: ... 
}

switch の後ろに型アサーション v := x.(type) を書きます。type は型 switch だけにしか使うことができません。そして、case に型を指定します。型 switch は case で指定した型と一致した節が選択され、その節の中で v は指定した型の値になります。

簡単な例を示しましょう。

リスト : 型 switch の使用例 (samaple94.go)

package main

import "fmt"

type Num interface {
    number()
}

type Int int

func (n Int) number() {}

type Real float64

func (n Real) number() {}

func sumOfNum(ary []Num) (Int, Real) {
    var sumi Int  = 0
    var sumr Real = 0.0
    for _, x := range ary {
        switch v := x.(type) {
        case Int:  sumi += v
        case Real: sumr += v
        }
    }
    return sumi, sumr
}

func main() {
    var ary []Num = []Num{
        Int(1), Real(1.1), Int(2), Real(2.2), Int(3), Real(3.3),
    }
    a, b := sumOfNum(ary)
    fmt.Println(a, b)
}
$ go run sample94.go
6 6.6

整数 (int) と実数 (float64) の両方を一つのスライスに混在させることを考えます。まず、数値を表す インターフェース Num を定義します。これでメソッド number を持つ型であれば何でも型 Num として扱うことができます。

int と float64 はメソッドを定義できないので、type で Int と Real という型を定義します。型は int, float64 と区別されますが、Int と Int、Real と Real の計算は int, float64 と同様に行うことができます。次に、Int と Real にメソッド number を定義します。処理は何も行っていませんが、これで Int と Real は型 Num として扱うことができます。

関数 sumOfNum はスライス ary の整数と実数の合計値を別々に求めて多値で返します。for ループの中の型 switch で条件分岐して、Int ならば sumi に値 v を加算し、Real ならば sumr に v を加算します。最後に return で sumi と sumr を返します。

●Stringer インターフェース

fmt パッケージの Print や Println はいろいろな型のデータを複数受け取って表示することができますが、これは Print, Println の引数が可変長引数で、引数の型が interface{ } になっているからです。そして、Print, Println は引数の型が次に示すインターフェース型であるかチェックします。

リスト : Stringer

type Stringer interface {
    String() string
}

引数が Stringer 型であればメソッド String を呼び出します。そうでなければ、デフォルトの表示を行います。インターフェース型のチェックも型アサーションで行うことができます。引数を x とすると、x.(Stringer) で x が Stringer 型かチェックすることができます。

メソッド String を実装すると、Print や Println の表示をカスタマイズすることができます。次の例を見てください。

リスト : Stringer の使用例 (sample95.go)

package main

import (
    "fmt"
    "strconv"
)

type myInt int

func (n myInt) String() string {
    return "myInt;" + strconv.Itoa(int(n))
}

func main() {
    n := myInt(10)
    m := myInt(20)
    fmt.Println(n + m)
    fmt.Println(n * m)
}
$ go run sample95.go
myInt;30
myInt;200

int に別名 myInt を付け、それにメソッド String を関連付けます。strconv パッケージには数値や文字列の変換を行う関数が多数用意されています。Itoa は int を文字列 (10 進数) に変換する関数です。これで myInt は数値の前に "myInt" が付加されて表示されます。

●インターフェースの埋め込み

構造体と同じく、インターフェースも他のインターフェースを埋め込むことができます。ただし、自分自身を埋め込むことはできません。

簡単な例を示しましょう。構造体 Foo とインターフェース FooI とメソッド getA、構造体 Bar とインターフェース BarI とメソッド getB、Foo と Bar を埋め込んだ構造体 Baz と、FooI と BarI を埋め込んだインターフェース BazI を定義します。

リスト : インターフェースの埋め込み (sample96.go)

package main

import "fmt"

// Foo
type Foo struct {
    a int
}

type FooI interface {
    getA() int
}

func (p *Foo) getA() int { return p.a }

// Bar
type Bar struct {
    b int
}

type BarI interface {
    getB() int
}

func (p *Bar) getB() int { return p.b }

// Baz
type Baz struct {
    Foo
    Bar
}

type BazI interface {
    FooI
    BarI
}

func main() {
    a := []FooI{
        &Foo{1}, &Foo{2}, &Baz{},
    }
    b := []BarI{
        &Bar{10}, &Bar{20}, &Baz{},
    }
    c := []BazI{
        &Baz{}, &Baz{Foo{1}, Bar{2}}, &Baz{Foo{3}, Bar{4}},
    }
    for i := 0; i < 3; i++ {
        fmt.Println(a[i].getA())
        fmt.Println(b[i].getB())
        fmt.Println(c[i].getA())
        fmt.Println(c[i].getB())
    }
}
$ go run sample96.go
1
10
0
0
2
20
1
2
0
0
3
4

Baz は Foo と Bar を埋め込んでいるので、メソッド getA, getB を引き継いでいます。BazI は FooI と BarI を埋め込んでいるので、メソッド getA, getB を持っている型であれば、BazI として扱うことができます。

スライス [ ]FooI を定義する場合、Baz も FooI のインターフェースを持っているので、Foo だけではなく Baz もスライスに格納することができます。ただし、Bar は格納できません。同様に、スライス [ ]BarI には Bar と Baz を格納することができますが、Foo は格納できません。スライス [ ]BazI には Baz しか格納できません。

●クイックソート (2)

それでは簡単な例として、再帰定義 で作成したクイックソートを、いろいろなデータに対応できるように修正してみましょう。汎用のソートプログラムは Go 言語の標準パッケージ sort に用意されていますが、私たちでも簡単にプログラムすることができます。

一番簡単な方法は、データの型によって比較関数をポリモーフィズムで選択することです。クイックソートの場合、x < y であれば true を返す比較関数があればアルゴリズムを実装することができます。この関数 (メソッド) を Less としましょう。インターフェースの定義は次のようになります。

リスト : 比較関数のインターフェース

type CmpI interface {
    Less(CmpI) bool
}

スライスの型を [ ]CmpI とすれば、要素の大小関係をメソッド Less で比較することができます。Less の引数の型は決めることができないので CmpI 型になることに注意してください。たとえば、int 型のスライスをソートしたい場合は次のように Less を定義します。

リスト : int の比較関数

type Cint int

func (n Cint) Less(m CmpI) bool {
    return n < m.(Cint)
}

type で int に別名 Cint を付け、Cint とメソッド Less を関連付けます。Less の引数はインターフェースなので、型アサーションで実際の値を求めて、レシーバの値と比較します。

クイックソートのプログラムは次のようになります。

リスト : クイックソート (1)

func quickSortCmpI(buff []CmpI, low, high int) {
    p := buff[low + (high - low) / 2]
    i, j := low, high
    for {
        for buff[i].Less(p) { i++ }
        for p.Less(buff[j]) { j-- }
        if i >= j { break }
        buff[i], buff[j] = buff[j], buff[i]
        i++
        j--
    }
    if low < i - 1  { quickSortCmpI(buff, low,   i - 1) }
    if high > j + 1 { quickSortCmpI(buff, j + 1, high) }
}

スライスの型は [ ]CmpI になります。要素を比較するときはメソッド Less を呼び出すだけなので、とくに難しいところはないと思います。

結論からいうと、このプログラムは遅いです。メソッド Less はポリモーフィズムにより選択され、Less の中では型アサーションで値を求めています。要素を比較するたびに、動的に値を求める処理が 2 回行われるので、実行速度はどうしても遅くなります。

そこで、もう一つの方法を試してみましょう。それは、標準パッケージ sort と同じインターフェースを使うことです。

リスト : ソートのインターフェース

type SortI interface {
    Len() int
    Less(int, int) bool
    Swap(int, int)
}

インターフェースの型名は SortI としました。標準パッケージ sort のインターフェース名とは異なるので注意してください。Len は配列 (スライス) の大きさを求めるメソッド、Less はスライスの 2 つの要素を比較するメソッド、Swap はスライスの要素を交換するメソッドです。Less と Swap は要素を位置で指定するところがポイントです。そして、これらのメソッドを配列 (スライス) に関連付けます。たとえば、int 型のスライスの場合は次のように定義します。

リスト : インターフェースの実装

type IntArray []int

func (ary IntArray) Len() int {
    return len(ary)
}

func (ary IntArray) Less(i, j int) bool {
    return ary[i] < ary[j]
}

func (ary IntArray) Swap(i, j int) {
    ary[i], ary[j] = ary[j], ary[i]
}

type で [ ]int に別名 IntArray を付け、それとメソッド Len, Less, Swap を関連付けます。レシーバの型はスライスなので、大きさは組み込み関数 len で簡単に求めることができます。Less で要素を比較する場合も、型アサーションを使わないで行うことができます。Swap も同様です。

クイックソートのプログラムは次のようになります。

リスト : クイックソート (2)

func quickSortSub(data SortI, low, high int) {
    p := low + (high - low) / 2
    i, j := low, high
    for {
        for data.Less(i, p) { i++ }
        for data.Less(p, j) { j-- }
        if i >= j { break }
        data.Swap(i, j)
        switch {
        case p == i: p = j
        case p == j: p = i
        }
        i++
        j--
    }
    if low < i - 1  { quickSortSub(data, low,   i - 1) }
    if high > j + 1 { quickSortSub(data, j + 1, high) }
}

func quickSort(data SortI) {
    quickSortSub(data, 0, data.Len() - 1)
}

quickSort の引数 data は SortI 型のインターフェースです。実際の処理は quickSortSub で行います。要素の比較はメソッド Less で行うため、枢軸の値は添字 p で管理します。i 番目と j 番目の要素を交換したとき、i または j が p と等しい場合は枢軸が移動したので、p の値を書き換えます。この処理を忘れると正常に動作しません。ご注意くださいませ。

あとは特に難しいところはないでしょう。詳細はプログラムリスト をお読みください。

●実行結果

それでは実際に試してみましょう。乱数で 100 万個の整数を生成し、それを 3 通りの方法でソートします。quickSortInt は [ ]int 型のスライスをソートします。quickSortCmpI は [ ]CmpI 型のスライスをソートし、quickSort は SortI 型のデータをソートします。

リスト : 簡単なテスト

func main() {
    a := make([]int,    1000000)
    b := make(IntArray, 1000000)
    c := make([]CmpI,   1000000)
    for i := 0; i < 1000000; i++ {
        x := rand.Int()
        a[i] = x
        b[i] = x
        c[i] = Cint(x)
    }
    s := time.Now()
    quickSortInt(a, 0, len(a) - 1)
    e := time.Now().Sub(s)
    fmt.Println(e)
    s = time.Now()
    quickSort(b)
    e = time.Now().Sub(s)
    fmt.Println(e)
    s = time.Now()
    quickSortCmpI(c, 0, len(a) - 1)
    e = time.Now().Sub(s)
    fmt.Println(e)
}

実行結果は次のようになりました。

    表 : 実行結果

quickSortInt  : 110.8166ms
quickSort     : 219.3039ms
quickSortCmpI : 317.883ms

実行環境 : Ubuntu 18.04 (WSL), Go ver 1.17.4, Intel Core i5-6200U 2.30GHz

quickSortInt が一番速く、次が quickSort で、quickSortCmpI が一番遅くなりました。ソートのインターフェースは標準パッケージ sort の方が優れていることがわかります。quickSort と quickSortCmpI はポリモーフィズムを使っているので、quikSortInt よりも遅くなるのは仕方がないところです。そうはいっても、100 万個のデータを 1 秒かからずにソートできるのですから、Go 言語はとても速いですね。びっくりしました。


●プログラムリスト

//
// quicksort.go : クイックソート
//
//                Copyright (C) 2014-2021 Makoto Hiroi
//
package main

import (
    "fmt"
    "math/rand"
    "time"
)

// []int をクイックソート
func quickSortInt(buff []int, low, high int) {
    pivot := buff[low + (high - low) / 2]
    i, j := low, high
    for {
        for pivot > buff[i] { i++ }
        for pivot < buff[j] { j-- }
        if i >= j { break }
        buff[i], buff[j] = buff[j], buff[i]
        i++
        j--
    }
    if low < i - 1  { quickSortInt(buff, low,   i - 1) }
    if high > j + 1 { quickSortInt(buff, j + 1, high) }
}

// ソートインターフェース
type SortI interface{
    Len() int
    Less(int, int) bool
    Swap(int, int)
}

// SortI によるクイックソート
func quickSortSub(data SortI, low, high int) {
    p := low + (high - low) / 2
    i, j := low, high
    for {
        for data.Less(i, p) { i++ }
        for data.Less(p, j) { j-- }
        if i >= j { break }
        data.Swap(i, j)
        switch {
        case p == i: p = j
        case p == j: p = i
        }
        i++
        j--
    }
    if low < i - 1  { quickSortSub(data, low,   i - 1) }
    if high > j + 1 { quickSortSub(data, j + 1, high) }
}

func quickSort(data SortI) {
    quickSortSub(data, 0, data.Len() - 1)
}

// インターフェースの実装
type IntArray []int

func (ary IntArray) Len() int {
    return len(ary)
}

func (ary IntArray) Less(i, j int) bool {
    return ary[i] < ary[j]
}

func (ary IntArray) Swap(i, j int) {
    ary[i], ary[j] = ary[j], ary[i]
}

// データを比較するインターフェース
type CmpI interface {
    Less(CmpI) bool
}

// []CmpI をクイックソート
func quickSortCmpI(buff []CmpI, low, high int) {
    p := buff[low + (high - low) / 2]
    i, j := low, high
    for {
        for buff[i].Less(p) { i++ }
        for p.Less(buff[j]) { j-- }
        if i >= j { break }
        buff[i], buff[j] = buff[j], buff[i]
        i++
        j--
    }
    if low < i - 1  { quickSortCmpI(buff, low,   i - 1) }
    if high > j + 1 { quickSortCmpI(buff, j + 1, high) }
}

// CmpI インターフェースを実装する
type Cint int

func (n Cint) Less(m CmpI) bool {
    return n < m.(Cint)
}

// 簡単なテスト
func main() {
    a := make([]int,    1000000)
    b := make(IntArray, 1000000)
    c := make([]CmpI,   1000000)
    for i := 0; i < 1000000; i++ {
        x := rand.Int()
        a[i] = x
        b[i] = x
        c[i] = Cint(x)
    }
    s := time.Now()
    quickSortInt(a, 0, len(a) - 1)
    e := time.Now().Sub(s)
    fmt.Println(e)
    s = time.Now()
    quickSort(b)
    e = time.Now().Sub(s)
    fmt.Println(e)
    s = time.Now()
    quickSortCmpI(c, 0, len(a) - 1)
    e = time.Now().Sub(s)
    fmt.Println(e)
}

初版 2014 年 3 月 9 日
改訂 2021 年 12 月 11 日

Copyright (C) 2014-2021 Makoto Hiroi
All rights reserved.

[ PrevPage | Golang | NextPage ]