はじめに
Goでの標準入力と、そこから文字列や整数値列を取得するための簡単な関数について説明しています。
全ソースコード
一番最後に全コードと使い方サンプルを掲載しました。「とりあえず、さっさと使いたい」方は、こちらをどうぞ。
(このコードを書いた時点でのGoコンパイラのバージョンは1.7.4です)
目的
どうも、こんにちは。Go言語修行僧です。
学習中の言語について、ただ淡々と本やサイトの完コピで進めていくのは非常に退屈ではありませんか?
自分で何か書こうとしても思いつかなかったり、思いついたものが現時点での自分のスキルを超えてしまって頓挫…などの経験はありませんか?
最近はpaizaのようなコンピュータ言語特化型学習サイトがあるのでとても便利な世の中になりました。
paiza自体は就職転職サービスの1つであり、IT系、ことプログラムを職業とする転職対象者による活用を推進する使い方ではあります。
しかしこうしたサイトは、ある言語を習得する際の時間縛り型問題集ととらえる考え方もできます。
そこで私は、paizaコーディングスキルチェックのDやCなどの低クラスあたりをかたっぱしからGoで書くという方法を取り、これを進めながらGoの習得を試みています。
一般に言語学習サイトの出題形式は、標準入力から文字列や数値列を受け取り、出題条件に従って処理加工を施した上で結果を標準出力するというスタイルが中心となっていますが、当初私はGoの標準入力や、空白などのデリミタで分割したり、取得した文字列をさらに整数値に変換するなどの方法がわからず、書いても書いてもコンパイルエラーで本題に進めず、Dクラスでさえ「タイムアップしても〜た〜(TT)」と泣いてしまったのでした(単純に準備不足…)。
C++ではiostreamを使ったcin、PHPではfgets(STDIN)など、簡単に標準入力ができるのですが、Goでの標準入力は若干手間がかかり、いちいち書いてたらそれだけで時間が無駄になりそうです。
そんなわけで最低限、文字列・整数値の標準入力、空白デリミタによる文字列・整数値分割の関数を用意して使い回すことにしました。
環境
私が実施した環境を記載しておきます。
ちなみにpaizaでのGoの実行環境はこちらに記載されています。
項目 | 値 |
---|---|
OS | macOS Sierra 10.12 |
Goバージョン | go1.7.4 darwin/amd64 |
関数の実装
Goによる標準入力の関数を実装していきます。
なおここでは、複数行入力後に複数行処理をするといった実装は試みていません。
文字列を取得する
まずは基本となる「文字列の1行入力」から実装しました。
package main
import (
"fmt"
"os"
"bufio"
"strings"
)
// 文字列を1行入力
func StrStdin() (stringInput string) {
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
stringInput = scanner.Text()
stringInput = strings.TrimSpace(stringInput)
return
}
func main() {
p := StrStdin()
fmt.Println(p)
}
StrStdin()関数が、標準入力から文字列を1行取得する関数です。関数名は特にこれでなくても良いですが、この後整数を返す関数も作っているので、あえてこういう名前にしています。
引数を取らず、string型を返します。つまり、StrStdin()関数の呼び出し元は、文字列を1つ受け取ることになります。
戻り値の変数名はstring型のstringInputとして関数に記載しているため、関数内部では宣言していません。
bufioパッケージからNewScanner()メソッドを使ってバッファからテキストを取得することになります。バッファ元はosパッケージであらかじめ定義されているStdinをディスクリプタとして使います。
参照:https://golang.org/pkg/os/#pkg-variables
scanner.Scan()メソッドでテキストがバッファリングされるまで待機し、バッファにテキストが入ったらscanner.Text()メソッドで文字列を取得します。
ここでは1行取得が目的なので、この記述をしています。複数行を連続的に取得したい場合には、末尾の余談1を参照してください。
stringsパッケージには便利な文字列処理メソッドが用意されているので、その中にあるTrimSpace()を使って、前後の余白と末尾の改行を取り除きます。トリムしたくない場合はこの処理をコメントアウトすればいいでしょう。
returnだけで変数名を指定していないのは、さきほどの関数名の脇に書いたstringInputをデフォルト戻り値変数としているためです。
main()関数などから、StrStdin()を呼び出して簡単に文字列を取得できます。
実行すると、以下のようになります。「alphonse」と入力したら、それが返されるので、そのまま表示されます。
% go run strstdin.go
alphonse
alphonse
空白が前後にあっても、トリムされて返されます。
% go run strstdin.go
alphonse
alphonse
整数値を1つ取得する
文字列が入力できれば怖いものナシでしょう。
StrStdin()をベースに、次々に関数を作ればいいだけですから。
package main
import (
"fmt"
"strings"
"os"
"bufio"
"strconv"
)
// 文字列を1行取得
func StrStdin() (stringReturned string) {
// ... 省略 ...
}
// 整数値1つ取得
func IntStdin() (int, error) {
stringInput := StrStdin()
return strconv.Atoi(strings.TrimSpace(stringInput))
}
func main() {
i, err := IntStdin()
if err != nil {
fmt.Println(err)
} else {
fmt.Println(i)
}
}
IntStdin()関数の中身は簡単で、戻された文字列をstrconvパッケージのAtoi()メソッドで整数値に変換しているだけです。
Atoi()は変換時エラーも一緒に戻してくれますので、ここではそのままエラーを呼び出し元に渡してエラーハンドリングを託しています。
main()関数は、IntStdin()の実装例です。変換エラーが来た場合はエラー文字列を表示しています。
実行例を見てみましょう。
% go run intstdin.go
123
123
当然ですが、トリミングにも対応します。
% go run intstdin.go
123
123
文字列が入力された場合は、Atoi()の変換エラーになるため、エラー文字列を表示して終わりになります。空文字でも同様です。
% go run intstdin.go
alphonse
strconv.ParseInt: parsing "alphonse": invalid syntax
IntStdin()を応用すれば、浮動小数点数も取得できると思いますので、実装にチャレンジしてみてはいかがでしょうか。
デリミタで区切られた文字列を分割して文字列配列(スライス)で取得する
文字列も整数値も取得できたところで、今度はいよいよデリミタ(区切り文字)による分割文字列取得です。
paizaなどのでのコーディングテストにおけるデータ入力では、以下のようなパターンの入力値が示されます。
3
al pho nse
mobile pol ice
pat lab or
この例では、最初にデータ入力件数として3行の入力があることを示す「3」から始まり、その後3行のデータが続くことを意味するデータ列になります。
そしてせっかくですので、以下の設問を仮定し、これに回答するプログラムを書いて見ることにします。
文字列をすべて繋げて1単語で出力するプログラムを書きましょう。
最初に繋げたいデータの個数が示されます。
続けて個数のぶんの行だけデータが来ます。データは空白で区切られた形で、ちぎれて入力されます。
空白を取り除き、1つの単語として出力してください。入力例)
3
al pho nse
mobile pol ice
pat lab or期待する出力)
alphonse
mobilepoilce
patlabor
この設問に回答するためのプログラムを書いてみましょう(以下を見ないで書いてもよいかと思います)。
package main
import (
// ... 省略 ...
)
// 文字列を1行取得
func StrStdin() (stringReturned string) {
// ... 省略 ...
}
// 整数値1つ取得
func IntStdin() (int, error) {
// ... 省略 ...
}
// 空白や空文字だけの値を除去したSplit()
func SplitWithoutEmpty(stringTargeted string, delim string) (stringReturned []string) {
stringSplited := strings.Split(stringTargeted, delim)
for _, str := range stringSplited {
if str != "" {
stringReturned = append(stringReturned, str)
}
}
return
}
// デリミタで分割して文字列スライスを取得
func SplitStrStdin(delim string) (stringReturned []string) {
// 文字列で1行取得
stringInput := StrStdin()
// 空白で分割
// stringSplited := strings.Split(stringInput, delim)
stringReturned = SplitWithoutEmpty(stringInput, delim)
return
}
func main() {
var words []string
len, _ := IntStdin()
for idx := 0; idx < len; idx += 1 {
wordlist := SplitStrStdin(" ")
words = append(words, strings.Join(wordlist, ""))
}
for _, word := range words {
fmt.Printf("%s\n", word)
}
}
SplitStrStdin()関数を作っています。ここでは引数で受け取ったデリミタ文字列(文字でも文字列でもいい)を使って、入力された文字列1行を区切る処理をしています。
実際にmain()関数などから呼ばれるのは、この関数です。
SplitStrStdin()はかなりシンプルになっています。すでに説明済みのStrStdin()関数から1行入力して、受け取った文字列をSplitWithoutEmpty()関数でデリミタで分割しています。この戻り値は文字列スライス(配列)になっています。
ところでSplitWithoutEmpty()はstringsパッケージのメソッドでもなければ組み込み関数でもなく、自作関数です。
SplitStrStdin()関数の上に定義されたSplitWithoutEmpty()関数は、デリミタで分割した文字列から、空文字を除去してスライスに保存するという処理をしています。
stringsパッケージにはSpilt()メソッドがあり、これはまさに文字列をデリミタで区切ってスライスを返してくれるという期待のメソッドなのですが、以下のように、複数の空白で区切られている入力の場合には、不都合が生じます。
foo bar baz
実験してみると、すぐにわかります。
package main
import (
"fmt"
"strings"
)
func main() {
str := "foo bar baz"
delim := " "
words := strings.Split(str, delim)
for i, word := range words {
fmt.Printf("%d) <%s>\n", i, word)
}
fmt.Printf("wordsの数=%d\n", len(words))
}
出力結果を見てみます。
% go run splittest.go
0) <foo>
1) <>
2) <bar>
3) <>
4) <baz>
wordsの数=5
1)と3)は、空の文字になっています。これはデリミタが半角空白文字1つだけであるため、strings.Split()が2つ連続した空白の間の空文字を1つの切り出し文字とみなしてスライスに保存しているためです。
paizaなどのコーディングテスト系の入力では、ほぼ入力値のデリミタは半角空白1文字なので、それだけ考えるなら、ここを気にしなくても問題はないでしょう。
とはいえ応用的ではないので、複数の連続したデリミタで生じる空文字を除去する処理を入れるためにラッパー関数を自作しました。ちなみにこの回避策(?)については余談2でbytesパッケージを使う方法も試みたので書いてみました。あんまり自信ないですが…。
戻りますと、ラッパー関数SplitWithoutEmpty()では、strings.Split()メソッドでデリミタで分割した文字列を文字列スライスで受け取った後、空文字でない場合のみ新たなスライスにappendで追加していくことで空文字以外のスライスを生成しています。
main()関数では、設問の回答を作るための処理を実装しています。あくまでも使用例なのでInsStdin()のエラー処理はしていません。
1行目の入力で受け取った整数値の分だけfor文を回し、SplitStrStdin()で空白で区切った文字列スライスをwordlistで受け取ります。
wordlistをstrings.Join()で結合文字列を空文字("")で結合したものをwordsにappendしていきます。
最後に3行分読み取った文字列の結果を出力して終わります。
% go run splitstrstdin.go
3
al pho nse
mobile pol ice
pat lab or
実行結果は以下のようになります。
alphonse
mobilepolice
patlabor
デリミタで区切られた数字列を分割して整数型配列(スライス)で取得する
最後に、デリミタで区切られた数字列を整数値のスライスとして取得する関数を実装して終わりにします。このぐらい準備しておけば、コーディングテスト系の実装には足りるでしょう。
数字列の例をいまさら示す必要もないかとは思いますが、こうした入力をプログラム内では計算処理をするために整数値として取り扱う必要があるため、整数型スライスで受け取りたいという希望があるでしょう。
10 11 12
2 20 200
3456 456 56
特別なことはしていません。すでに示した関数を使った処理をしているにすぎません。
SplitStrStdin()関数を使って得られた数字列のスライスをAtoi()で整数値に変換しつつ整数型スライスにappendしているという処理を加えています。
package main
import (
// ... 省略 ...
)
// 文字列を1行取得
func StrStdin() (stringReturned string) {
// ... 省略 ...
}
// 空白や空文字だけの値を除去したSplit()
func SplitWithoutEmpty(stringTargeted string, delim string) (stringReturned []string) {
// ... 省略 ...
}
// デリミタで分割して文字列スライスを取得
func SplitStrStdin(delim string) (stringReturned []string) {
// ... 省略 ...
}
// デリミタで分割して整数値スライスを取得
func SplitIntStdin(delim string) (intSlices []int, err error) {
// 文字列スライスを取得
stringSplited := SplitStrStdin(" ")
// 整数スライスに保存
for i := range stringSplited {
var iparam int
iparam, err = strconv.Atoi(stringSplited[i])
if err != nil {
return
}
intSlices = append(intSlices, iparam)
}
return
}
func main() {
i, err := SplitIntStdin(" ")
if err != nil {
fmt.Println(err)
} else {
fmt.Println(i)
fmt.Println(len(i))
}
}
main()関数では、SplitIntStdin()を呼び出し、エラーかどうかで分岐をして表示内容を区別しています。
実行して見ます。入力する数字列の幅はいくつでも問題ありません。
% go run splitintstdin.go
123 234 345
[123 234 345]
スライス数=3
% go run splitintstdin.go
012 023 034
[12 23 34]
スライス数=3
% go run splitintstdin.go
123 234 345 456
[123 234 345 456]
スライス数=4
数字の間に文字が入っているなど、数字列ではない場合はAtoi()の変換エラーとなるため、エラーになります。
% go run splitintstdin.go
123 foo 345
strconv.ParseInt: parsing "foo": invalid syntax
% go run splitintstdin.go
012 8xx 999
strconv.ParseInt: parsing "8xx": invalid syntax
以上の実装で、コーディングテストへの対応はおおむね問題がないかと思います。
余談
余談コーナーです。読み飛ばしてください…。
余談1) 複数行を連続的に読み込む場合の処理
連続して複数行の文字列を取得する場合は、for文を用います。scanner.Scan()はboolを返すので、たとえば以下のように使用することで複数行を取得できるでしょう。
package main
import (
"fmt"
"os"
"bufio"
)
func main() {
b := true
scanner := bufio.NewScanner(os.Stdin)
for b {
b = scanner.Scan()
t := scanner.Text()
fmt.Printf("%v, %s\n", b, t)
if t == "終わり" {
break;
}
}
}
if文で入力文字「終わり」と比較しているのは、Ctrl-Cなどで終了ができなくなるためで、EOFの入力ができなくなるとforループから脱出できないので、予防措置として書いてあります(別に何の文字・文字列でも良い)。
余談2) strings.Split()での不都合の回避策(?)
strings.Split()だと不都合が生じることの回避策としてbytes.NewBufferString型で対応するという方法もあるとは思いますが、個人的にはパフォーマンスに見合わなそうなので、以下実験という形でのみ理解するに留めています。
また、この手の回避策は他の方法論もあるとは思います。調べたら結構キリがなさそうで深そうですね…。
package main
import (
"fmt"
"bytes"
)
func main() {
var words []string
src := bytes.NewBufferString("al pho nse")
delim := bytes.NewBufferString(" ")
byteWords := bytes.Split(src.Bytes(), delim.Bytes())
for _, byteWord := range byteWords {
buf := bytes.NewBuffer(byteWord)
words = append(words, buf.String())
}
// 空白がないことを示すために<>で囲っている
fmt.Printf("<%s><%s><%s>\n", words[0], words[1], words[2])
}
全ソースコードと使用サンプル(main()関数)
「細かい説明なんかどうでもいい! paizaで使いたいんだから、さっさとサンプルコードよこせ!」という向きに、以下を貼っておきます。コピペして使用して頂いても改良して頂いても問題ありません。
package main
import (
"fmt"
"strings"
"os"
"bufio"
"strconv"
)
// 文字列を1行取得
func StrStdin() (stringReturned string) {
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
stringInput := scanner.Text()
stringReturned = strings.TrimSpace(stringInput)
return
}
// 整数値1つ取得
func IntStdin() (int, error) {
stringInput := StrStdin()
return strconv.Atoi(strings.TrimSpace(stringInput))
}
// 空白や空文字だけの値を除去したSplit()
func SplitWithoutEmpty(stringTargeted string, delim string) (stringReturned []string) {
stringSplited := strings.Split(stringTargeted, delim)
for _, str := range stringSplited {
if str != "" {
stringReturned = append(stringReturned, str)
}
}
return
}
// デリミタで分割して文字列スライスを取得
func SplitStrStdin(delim string) (stringReturned []string) {
// 文字列で1行取得
stringInput := StrStdin()
// 空白で分割
// stringSplited := strings.Split(stringInput, delim)
stringReturned = SplitWithoutEmpty(stringInput, delim)
return
}
// デリミタで分割して整数値スライスを取得
func SplitIntStdin(delim string) (intSlices []int, err error) {
// 文字列スライスを取得
stringSplited := SplitStrStdin(" ")
// 整数スライスに保存
for i := range stringSplited {
var iparam int
iparam, err = strconv.Atoi(stringSplited[i])
if err != nil {
return
}
intSlices = append(intSlices, iparam)
}
return
}
//////////////////////////////////////////
// 使い方の例
//////////////////////////////////////////
func main() {
// 入力例)
// alphonse
// alphonse
// alphonse (後ろにも空白がある)
// 出力)
// alphonse
p := StrStdin()
fmt.Println(p)
// 入力例)
// 123
// 123
// 123 (後ろにも空白がある)
// 出力)
// 123
i, err := IntStdin()
if err != nil {
// paizaなどではエラー処理は不要かもしれない
fmt.Println(err)
}
fmt.Println(i)
// 入力例)
// foo bar baz
// foo bar baz
// 出力)
// foo
// bar
// baz
pp := SplitStrStdin(" ")
for _, s := range pp {
fmt.Printf("%s\n", s)
}
// 入力例)
// 123 042 9999
// 123 042 9999
// 出力)
// 123
// 42
// 9999
ii, eerr := SplitIntStdin(" ")
if eerr != nil {
fmt.Println(eerr)
}
for _, i := range ii {
fmt.Printf("%d\n", i)
}
}