【Go】A Tour of GoのExercise: Stringersを解いた

投稿日:

もうすっかり春ですね。
というわけで、最近GoをやっていきということでA Tour of Goをコツコツと解いています。
shimabox/study-go でやっています

このA Tour of Goは演習問題がところどころにあり、自分のようにGoまったくわからんという人間でも、やれば少しずつGoと会話できるようになる素晴らしいコンテンツになっています(自分はまだ終わっていませんが)。

今回はこの演習問題の中でExercise: Stringersを解いたときの自分のつまづきについて書いてみたいと思います。

最初に解いた方法

最初に解いた方法が以下の通りです。

exercise-stringers.go

package main

import "fmt"

type IPAddr [4]byte

func (ip IPAddr) String() string {
	// IPAddrは[4]byteなので愚直にこれでもいいはず
	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)
	}
}
  • type IPAddr [4]byte という型がある
  • 4つのbyteが入っていると決まっている
  • であれば、愚直に fmt.Sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]) でええやん

という発想で解きました。
実行した結果は以下のとおりです。

$ go run exercise-stringers.go
loopback: 127.0.0.1
googleDNS: 8.8.8.8

なんか合ってそう。
Go Playground – The Go Programming Language

ちょっと考えてみる

だがしかし、"%d.%d.%d.%d", ip[0], ip[1], ip[2]とハードコーティングされているところが気になります。

例えば、仮にtype IPAddr [3]byteとなったとき、あっちこっち修正しなくてはいけなくなるので、うまいことハードコーティングを避ける方法を考えてみたいと思います。

※ IPアドレスを表現したIPAddr [4]byteなんだからそんなことは考えなくていいと言われればそれまでなのですが、それだと試合終了だし他に似たケースも出てくるかもしれないので話を続けます

次に解いた方法

そこで、次に解いた方法がこちらになります。

type IPAddr [4]byte

func (ip IPAddr) String() string {
	list := []string{}
	for _, v := range ip {
		list = append(list, string(v))
	}
	// . で結合して返す
	return fmt.Sprint(strings.Join(list, "."))
}
  • スライス([]string{})を用意
  • IPAddrをforでまわして値をスライスに追加
  • IPAddrの要素vはbyteなのでstring()でキャスト
    • 注: これがそもそも違います
  • スライスに格納されている要素を"."で結合して返す

では、実行してみます。

$ go run exercise-stringers.go
loopback: ...
googleDNS:.

なんということでしょう。わけわからん結果になりました。
Go Playground – The Go Programming Language

string()で整数をキャストすると空文字が返る

ちょっと中身を出力してみます。

for _, v := range ip {
	fmt.Printf("%v, %v, %T\n", v, string(v), string(v))
	list = append(list, string(v))
}

こんなん出ました。

go run exercise-stringers.go
127, , string
0, , string
0, , string
1, , string
loopback: ...
8,, string
8,, string
8,, string
8,, string
googleDNS:.

見事にstring(v)が空です。

string()で数値はキャストできない

はい。お察しの通り、string()で数値はキャストできません。
type string の初期化をやっているようなものだからなのかな。。
(なんか手癖で書いちゃったな)

s := string(1)

実際にこう書いてみると、

conversion from untyped int to string yields a string of one rune, not a string of digits (did you mean fmt.Sprint(x)?)stringintconv
// Google翻訳
型付けされていないintから文字列への変換は、数字の文字列ではなく、1つのルーンの文字列を生成します(fmt.Sprint(x)のことでしょうか)。

このように注釈がでるはずです。
なので、string()でやっても引数がbyteなので空文字(rune型?)になっていたということになります。

rune型とは

ここまで調べるとなると、この記事の範疇を超えてしまいそうなのでググってほしいのですが、かんたんにいうとコードポイントで表現された値のようです。

ちょっとこのへん読んでも理解が難しいのですが、単純に使い方が悪かったと今は思うことにします。

最終的に解いた方法

文字列へのキャストの方法が悪いことがわかったので、以下のようにして対応しました。

type IPAddr [4]byte

func (ip IPAddr) String() string {
	list := []string{}
	for _, v := range ip {
		// fmt.Printf("%T", v) => uint8(byte)
		// vはuint8なのでstringに変換
		// int(v)でintにしてからstrconv.Itoa()してappend
		list = append(list, strconv.Itoa(int(v)))
	}
	// . で結合して返す
	return fmt.Sprint(strings.Join(list, "."))
}
  • 整数から文字列へのキャストはstrconv.Itoaを使う
  • Itoa(i int)なのでint(v)する
    • uint8からintへのキャストなので問題なし
  • fmt.Sprint(v)でもいけます
  • こうすると、type IPAddr [3]byteでもいけるし、type IPAddr []byteでもいけます
    • 他の箇所の修正は必要ですが、修正箇所は減るはず
    • メモリ効率を考えると後者の[]byteはアレかもしれませんが、まぁそれはそれとして

実行した結果は以下のとおりです。

$ go run exercise-stringers.go
loopback: 127.0.0.1
googleDNS: 8.8.8.8

GJ。
Go Playground – The Go Programming Language

まとめ

  • 整数から文字列へのキャストはstrconv.Itoa(i int)を使う
  • string(i int)だと空文字(rune型?)が返る
  • 無知は辛い
  • Go難しいけど楽しい

おわりに

今までずーーーっとほぼPHP(Python, jsもたまに)を書いてきて、このままじゃアカンとGoの勉強を始めてみました。
型でいうとPHPもjs,TypeScriptも型はありますが(Pythonは型ヒントかしら)、やっぱりきちんとした?静的言語ということでGoは今までの感覚で書くことが出来ず歯がゆい感じがします。
でも、なんでしょう、プログラミングを覚えたてのような、あのピュアな感じを思い出しながらやれているので楽しいです。

とりあえず、今月中にはA Tour of Goを終わらせようと思っています。

参考

作成者: shimabox

Web系のプログラマをやっています。 なるべく楽しく生きていきたい。

コメントする

メールアドレスが公開されることはありません。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください