A Tour of Go の Exercise: Stringers 解答例

はじめに

Go 言語基礎文法最速マスターにて出題されている演習課題「Exercise: Stringers(ストリンガー)」の答えに辿り着くまでの手順と解答例の解説を記事にしてみました。暗黙のインターフェース、型にメソッドを定義する、レシーバーなどについても簡単に説明しています。

課題

課題の目的は IPAddr 型の IP アドレス {1, 2, 3, 4}ドット 10 進表記"1.2.3.4" で出力する fmt.Stringer インターフェースを実装することです。

package main

import "fmt"

type IPAddr [4]byte

// TODO: Add a "String() string" method to IPAddr.

func main() {
	hosts := map[string]IPAddr{
		"loopback":  {127, 0, 0, 1},
		"googleDNS": {8, 8, 8, 8},
	}
	for name, ip := range hosts {
		fmt.Printf("%v: %v\n", name, ip)
	}
}

解答例

package main

import "fmt"

type IPAddr [4]byte

func (ip IPAddr) String() string {
	return fmt.Sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3])
}

func main() {
	hosts := map[string]IPAddr{
		"loopback":  {127, 0, 0, 1},
		"googleDNS": {8, 8, 8, 8},
	}
	for name, ip := range hosts {
		fmt.Printf("%v: %v\n", name, ip)
	}
}

解説

初めに、課題のソースコードを読み解きます。

5 行目、type[4]byte 型を IPAddr という名前で 型宣言 していますね。

type IPAddr [4]byte

type とは、既存の型と同じ基になる型と操作を持つ新しい別個の型を作成して、それに識別子をバインドすることができる予約語です。

type 識別子 型

わかりやすく言うと、既存の型と同じの新しい名前の型を作っちゃおうぜウェーイwwwってことです。

7 行目、TODO コメントに IPAddr 型に String() string メソッドを追加してねと書いてあります。

TODO: Add a "String() string" method to IPAddr.

このコメントの後に処理を実装することがわかります。

9 行目、main 関数で map[string]IPAddr 型の変数を定義して、for range 文で各要素を fmt.Printf 関数で出力しています。

func main() {
	hosts := map[string]IPAddr{
		"loopback":  {127, 0, 0, 1},
		"googleDNS": {8, 8, 8, 8},
	}
	for name, ip := range hosts {
		fmt.Printf("%v: %v\n", name, ip)
	}
}

変数 ip にドット 10 進記法で出力する IP アドレスが格納されていることがわかります。

さて、実装を進める前に interface(インターフェース)型の理解が必要になります。

interface 型とは method(メソッド)の signature(シグネチャ)の集まりを定義したものです。

シグネチャとはメソッドの名前、引数の数と型、戻り値の型のことです。

interface 型は type で下記のように宣言することができます、Java のように明示的に implements と書いて宣言する必要はありません。

type 識別子 interface

では typefmt.Stringer インターフェースを実装すれば良いかというと、違います。

Stringers でも説明されているように、fmt.Stringer インターフェースは既に fmt パッケージで下記のように宣言されています。

type Stringer interface {
	String() string
}

つまり、やることは TODO コメントにあった通り IPAddr 型に String() string メソッドを追加するだけです。

メソッドの追加ってどうやりゃいいんじゃって話なんですけど、ここで method(メソッド)の理解が必要になります。

Go は型にメソッドを定義できます。

メソッドとは、receiver(レシーバー)引数を伴う関数のことです。

レシーバーとは、下記のメソッドの定義では ip の部分になります。

func (ip IPAddr) String() string {}

Go はインターフェースに定義された関数をメソッドとして定義すると、暗黙的にインターフェースを実装したことになります。

前述のコードは String 関数をメソッドとして定義することで fmt.Stringer インターフェースを暗黙的に実装しています。もちろん、fmf パッケージを import している前提の話です。

つまり、前述のコードは課題である fmt.Stringer インターフェースを実装するということを実現することができます。

しかし、まだ IP アドレスをドット 10 進表記にするという課題が残っていますね。

main 関数では、変数 ip に IP アドレスを格納して fmt.Printf 関数で出力していました。

ということは、下記のコードが書いてあると fmt.Printf 関数は変数 ip を出力する際に (ip IPAddr) String() を呼び出します。

func (ip IPAddr) String() string {}

なんで fmt.Printf 関数から String 関数が呼び出されるんじゃ?ってなりますよね。

Stringer インターフェースの ソースコード にあるコメントに答えがあります。Stringers でも説明されているように、多くのパッケージでは変数を文字列で出力する為に Stringer インターフェースが使われています。

// Stringer is implemented by any value that has a String method,
// which defines the ``native'' format for that value.
// The String method is used to print values passed as an operand
// to any format that accepts a string or to an unformatted printer
// such as Print.
type Stringer interface {
	String() string
}

また、fmt パッケージドキュメントの Printing にて、print 関数について下記のような説明があります。

オペランドが String メソッドを実装している場合は、オブジェクトを文字列に変換する為にそのメソッドが呼び出され、その後(もしあれば)動詞の要求に応じてフォーマットされます。

If an operand implements method String() string, that method will be invoked to convert the object to a string, which will then be formatted as required by the verb (if any).

https://pkg.go.dev/fmt@go1.17.2

この辺りの理解が大事です。

では、どうやってレシーバー引数の IP アドレスをドット 10 進表記に変換するのかというと fmt.Sprintf 関数を使います。

func (ip IPAddr) String() string {
	return fmt.Sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3])
}

fmt.Sprintf 関数は、書式指定子に従ってフォーマットした結果を文字列で返します。%d という書式指定子を指定すると 10 進数で出力してくれます。

あとは、配列の添字を指定してドッド 10 進表記にして返すように実装するだけです。

package main

import "fmt"

type IPAddr [4]byte

func (ip IPAddr) String() string {
	return fmt.Sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3])
}

func main() {
	hosts := map[string]IPAddr{
		"loopback":  {127, 0, 0, 1},
		"googleDNS": {8, 8, 8, 8},
	}
	for name, ip := range hosts {
		fmt.Printf("%v: %v\n", name, ip)
	}
}

実行すると下記のように出力されます。

loopback: 127.0.0.1
googleDNS: 8.8.8.8

大丈夫だ、問題ない。

以上です。

おわりに

仮面ライダーストリンガーっていそうじゃない。え、カブトムシの改造電気人間がいるの?