Goで文字列と数値を変換するには、標準ライブラリの strconv パッケージを使います。文字列から int は strconv.Atoi、int から文字列は strconv.Itoa が基本で、型や基数を細かく決めたいときは ParseInt / FormatInt などを選びます。 string(65) のような型変換は数字の文字列にはならず、文字コード 65 の "A" になる点が最初のつまずきどころです。

この記事では、string と int / int64 / uint / float64 / bool の相互変換を「変換元 → 変換先 → 関数 → コード」の早見表にまとめ、go1.24.7 で実行した結果をそのまま載せています。 空文字・全角数字・前後の空白・桁あふれで出るエラーメッセージ、errors.Is での判別、base と bitSize の意味、fmt.Sprint との使い分けまで、コピペで使える関数と一緒に確認できます。

文字列⇔数値変換の早見表

変換元 変換先 関数 コード
string int strconv.Atoi n, err := strconv.Atoi("123")
string int64 strconv.ParseInt n, err := strconv.ParseInt("123", 10, 64)
string int32 など strconv.ParseInt と型変換 n, err := strconv.ParseInt(s, 10, 32) のあと int32(n)
string uint64 strconv.ParseUint u, err := strconv.ParseUint("42", 10, 64)
string float64 strconv.ParseFloat f, err := strconv.ParseFloat("3.14", 64)
string bool strconv.ParseBool b, err := strconv.ParseBool("true")
string(16進) int64 strconv.ParseInt n, err := strconv.ParseInt("ff", 16, 64)
int string strconv.Itoa s := strconv.Itoa(123)
int64 string strconv.FormatInt s := strconv.FormatInt(n, 10)
uint64 string strconv.FormatUint s := strconv.FormatUint(u, 10)
float64 string strconv.FormatFloat s := strconv.FormatFloat(f, 'f', -1, 64)
float64 string(小数2桁) strconv.FormatFloat s := strconv.FormatFloat(f, 'f', 2, 64)
bool string strconv.FormatBool s := strconv.FormatBool(true)
int string(2進・16進) strconv.FormatInt s := strconv.FormatInt(255, 16)
なんでも string fmt.Sprint s := fmt.Sprint(v)
int string(ゼロ埋め) fmt.Sprintf s := fmt.Sprintf("%05d", n)
int int64 / float64 型変換 int64(n)、float64(n)
float64 int 型変換(小数は切り捨て) int(f)
int(文字コード) string(1文字) 型変換 string(rune(65))

文字列から数値への関数は必ず 値と error の 2 つ を返します。数値から文字列への関数は error を返しません。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Goの文字列⇔数値変換|strconv Atoi・ParseInt早見表」

検証環境

項目 内容
Go go1.24.7 linux/amd64
実行方法 go run main.go(go vet の確認だけ go.mod つき)
外部モジュール なし(標準ライブラリのみ)
int の大きさ 64 ビット

int の大きさは環境で決まります。32 ビット環境では int の範囲が狭くなるため、桁あふれの境界が変わります(32 ビット環境は未実行)。

基本の変換をまとめて動かす

早見表の関数を、型と値がわかる形でまとめて実行します。

package main

import (
    "fmt"
    "strconv"
)

func main() {
    // ---- 文字列 → 数値・真偽値 ----
    i, err := strconv.Atoi("123")
    fmt.Printf("Atoi        %T %v %v\n", i, i, err)

    i64, err := strconv.ParseInt("-9000000000", 10, 64)
    fmt.Printf("ParseInt    %T %v %v\n", i64, i64, err)

    u64, err := strconv.ParseUint("42", 10, 64)
    fmt.Printf("ParseUint   %T %v %v\n", u64, u64, err)

    f, err := strconv.ParseFloat("3.14", 64)
    fmt.Printf("ParseFloat  %T %v %v\n", f, f, err)

    b, err := strconv.ParseBool("true")
    fmt.Printf("ParseBool   %T %v %v\n", b, b, err)

    // ---- 数値・真偽値 → 文字列 ----
    fmt.Printf("Itoa        %q\n", strconv.Itoa(123))
    fmt.Printf("FormatInt   %q\n", strconv.FormatInt(-9000000000, 10))
    fmt.Printf("FormatUint  %q\n", strconv.FormatUint(42, 10))
    fmt.Printf("FormatFloat %q\n", strconv.FormatFloat(3.14, 'f', -1, 64))
    fmt.Printf("FormatBool  %q\n", strconv.FormatBool(true))

    // ---- 数値どうし ----
    var n int = 300
    fmt.Println("int→int64  ", int64(n))
    fmt.Println("int→float64", float64(n)/7)
    x, y := 3.99, -3.99
    fmt.Println("float→int  ", int(x), int(y))
    fmt.Println("int→int8   ", int8(n))
    fmt.Println("int→uint   ", uint(n))
    m := -1
    fmt.Println("-1→uint8   ", uint8(m))
}

実行結果:

Atoi        int 123 <nil>
ParseInt    int64 -9000000000 <nil>
ParseUint   uint64 42 <nil>
ParseFloat  float64 3.14 <nil>
ParseBool   bool true <nil>
Itoa        "123"
FormatInt   "-9000000000"
FormatUint  "42"
FormatFloat "3.14"
FormatBool  "true"
int→int64   300
int→float64 42.857142857142854
float→int   3 -3
int→int8    44
int→uint    300
-1→uint8    255
  • Atoi は int、ParseInt は int64、ParseUint は uint64、ParseFloat は float64 を返します。ParseInt の第3引数に 32 を渡しても、戻り値の型は int64 のままです(後述)。
  • float64 から int への型変換は、四捨五入ではなく小数部の切り捨て です。3.99 は 3、-3.99 は -3 になります。四捨五入したいときは int(math.Round(x)) を使います。
  • 数値どうしの型変換は範囲を確かめません。300 を int8 にすると 44、-1 を uint8 にすると 255 になり、エラーは出ません。
  • 定数をそのまま int(3.99) と書くと、cannot convert 3.99 (untyped float constant) to type int というコンパイルエラーになります。変数に入れた値であれば変換できます。

Atoi で出るエラーを再現する(空文字・全角数字・空白・桁あふれ)

入力フォームや CSV から受け取った文字列は、数字だけとは限りません。Atoi にいろいろな文字列を渡して、戻り値とエラーメッセージを集めました。

package main

import (
    "errors"
    "fmt"
    "strconv"
)

func main() {
    inputs := []string{"", "123", " 42", "42\n", "12a", "1.5", "1,234", "+5", "-0", "0x1F", "1_000", "9223372036854775808"}
    for _, s := range inputs {
        n, err := strconv.Atoi(s)
        kind := "OK"
        switch {
        case errors.Is(err, strconv.ErrSyntax):
            kind = "ErrSyntax"
        case errors.Is(err, strconv.ErrRange):
            kind = "ErrRange"
        }
        fmt.Printf("%-22q 値=%-20d %-9s %v\n", s, n, kind, err)
    }
}

実行結果:

""                     値=0                    ErrSyntax strconv.Atoi: parsing "": invalid syntax
"123"                  値=0                    ErrSyntax strconv.Atoi: parsing "123": invalid syntax
" 42"                  値=0                    ErrSyntax strconv.Atoi: parsing " 42": invalid syntax
"42\n"                 値=0                    ErrSyntax strconv.Atoi: parsing "42\n": invalid syntax
"12a"                  値=0                    ErrSyntax strconv.Atoi: parsing "12a": invalid syntax
"1.5"                  値=0                    ErrSyntax strconv.Atoi: parsing "1.5": invalid syntax
"1,234"                値=0                    ErrSyntax strconv.Atoi: parsing "1,234": invalid syntax
"+5"                   値=5                    OK        <nil>
"-0"                   値=0                    OK        <nil>
"0x1F"                 値=0                    ErrSyntax strconv.Atoi: parsing "0x1F": invalid syntax
"1_000"                値=0                    ErrSyntax strconv.Atoi: parsing "1_000": invalid syntax
"9223372036854775808"  値=9223372036854775807  ErrRange  strconv.Atoi: parsing "9223372036854775808": value out of range
入力 結果 種別 補足
空文字 0 とエラー ErrSyntax 未入力は 0 にはならずエラーになる
全角の 123 0 とエラー ErrSyntax 半角に直してから渡す
前に空白 0 とエラー ErrSyntax strings.TrimSpace で取り除く
末尾に改行 0 とエラー ErrSyntax 標準入力やファイルから読んだ行で起きやすい
12a、1.5、1,234 0 とエラー ErrSyntax 途中まで読んで 12 を返す、という動きはしない
+5、-0 5、0 OK 先頭の符号は受け付ける
0x1F、1_000 0 とエラー ErrSyntax Atoi は 10 進のみ。base に 0 を渡す ParseInt なら読める
9223372036854775808 最大値とエラー ErrRange int64 の最大値を 1 超えている

エラーメッセージは「strconv.関数名: parsing "入力": 理由」の形 で、理由は invalid syntax(書式が合わない)と value out of range(範囲を超えた)の 2 種類が中心です。桁あふれのときは、0 ではなく その型の最大値(または最小値)とエラーの両方 が返ります。エラーを確かめずに値だけ使うと、上限に張り付いた値がそのまま流れていくので、必ず err を先に確認します。

errors.Is で ErrSyntax と ErrRange を判別する(コピペ用関数つき)

利用者に見せるメッセージを「数字で入力してください」と「桁数が多すぎます」に分けたいときは、errors.Is で strconv.ErrSyntax と strconv.ErrRange を見分けます。 前後の空白や全角数字を整える関数、既定値を返す関数と合わせて、コピペで使える形にまとめました。

package main

import (
    "errors"
    "fmt"
    "strconv"
    "strings"
)

// ToInt は前後の空白と全角数字を整えてから int に変換する
func ToInt(s string) (int, error) {
    s = strings.TrimSpace(s)
    s = strings.Map(func(r rune) rune {
        switch {
        case r >= '0' && r <= '9':
            return r - '0' + '0'
        case r == '-' || r == 'ー':
            return '-'
        case r == ',' || r == ',':
            return -1 // 桁区切りは取り除く
        }
        return r
    }, s)
    return strconv.Atoi(s)
}

// AtoiOr は変換できないときに既定値を返す
func AtoiOr(s string, def int) int {
    n, err := strconv.Atoi(s)
    if err != nil {
        return def
    }
    return n
}

// ParseQty は 1〜max の範囲の数量だけを受け付け、理由を言葉で返す
func ParseQty(s string, max int) (int, error) {
    n, err := strconv.Atoi(strings.TrimSpace(s))
    switch {
    case errors.Is(err, strconv.ErrSyntax):
        return 0, fmt.Errorf("数量 %q は数字で入力してください", s)
    case errors.Is(err, strconv.ErrRange):
        return 0, fmt.Errorf("数量 %q は桁数が多すぎます", s)
    case err != nil:
        return 0, err
    case n < 1 || n > max:
        return 0, fmt.Errorf("数量は 1〜%d で入力してください(入力: %d)", max, n)
    }
    return n, nil
}

// ToInt32 は範囲を確かめてから int32 にする
func ToInt32(s string) (int32, error) {
    n, err := strconv.ParseInt(s, 10, 32)
    return int32(n), err
}

func main() {
    for _, s := range []string{" 42 ", "123", "1,234", "-5", "12a", "\t7\n"} {
        n, err := ToInt(s)
        fmt.Printf("ToInt(%q) = %d, %v\n", s, n, err)
    }
    n, err := ToInt(" 8 ") // 前後は全角空白
    fmt.Println("全角空白つき:", n, err)
    fmt.Println(AtoiOr("15", 10), AtoiOr("", 10), AtoiOr("abc", 10))
    for _, s := range []string{"3", " 3 ", "abc", "", "0", "1000", "99999999999999999999"} {
        n, err := ParseQty(s, 99)
        fmt.Printf("ParseQty(%q) = %d, %v\n", s, n, err)
    }
    fmt.Println(ToInt32("2147483647"))
    fmt.Println(ToInt32("2147483648"))
    // 範囲を確かめない変換は値が変わる
    big, _ := strconv.ParseInt("2147483648", 10, 64)
    fmt.Println("int32(2147483648) =", int32(big))
}

実行結果:

ToInt(" 42 ") = 42, <nil>
ToInt("123") = 123, <nil>
ToInt("1,234") = 1234, <nil>
ToInt("-5") = -5, <nil>
ToInt("12a") = 0, strconv.Atoi: parsing "12a": invalid syntax
ToInt("\t7\n") = 7, <nil>
全角空白つき: 8 <nil>
15 10 10
ParseQty("3") = 3, <nil>
ParseQty(" 3 ") = 3, <nil>
ParseQty("abc") = 0, 数量 "abc" は数字で入力してください
ParseQty("") = 0, 数量 "" は数字で入力してください
ParseQty("0") = 0, 数量は 1〜99 で入力してください(入力: 0)
ParseQty("1000") = 0, 数量は 1〜99 で入力してください(入力: 1000)
ParseQty("99999999999999999999") = 0, 数量 "99999999999999999999" は桁数が多すぎます
2147483647 <nil>
2147483647 strconv.ParseInt: parsing "2147483648": value out of range
int32(2147483648) = -2147483648
関数 役割 使いどころ
ToInt 空白・全角数字・桁区切りを整えてから変換 人が手で入力した値
AtoiOr 変換できなければ既定値を返す 省略できる設定値やクエリパラメータ
ParseQty エラーの種類ごとに日本語のメッセージを返す 入力チェック
ToInt32 bitSize で範囲を確かめてから int32 にする 列の型が 32 ビットの値
  • strings.TrimSpace は半角空白・タブ・改行に加えて、全角空白(U+3000)も取り除きます。出力の「全角空白つき: 8」がその結果です。
  • 全角数字や桁区切りをどこまで受け付けるかは、画面の仕様として決めておく部分です。受け付けない方針なら、TrimSpace だけにして ErrSyntax のメッセージを返します。
  • err == strconv.ErrSyntax という比較は false になります。 strconv が返すのは、ErrSyntax を包んだ *strconv.NumError だからです。比較には errors.Is を使います(次の章の出力の最終行で確認しています)。

エラーの中身を取り出したいときは errors.As を使います。NumError には Func(関数名)、Num(入力)、Err(理由)の 3 つのフィールドがあります。

_, err := strconv.ParseInt("12a", 10, 64)
var ne *strconv.NumError
if errors.As(err, &ne) {
    fmt.Printf("Func=%q Num=%q Err=%v\n", ne.Func, ne.Num, ne.Err)
}

base と bitSize の意味を出力で確かめる

ParseInt と ParseUint は (s, base, bitSize)、ParseFloat は (s, bitSize) を引数に取ります。

引数 意味 指定できる値
base 何進数として読むか 2〜36、または 0(接頭辞から自動で判定)
bitSize(整数) 結果が収まるべき型の大きさ 0(int)、8、16、32、64
bitSize(小数) float32 と float64 のどちらの精度で読むか 32、64

bitSize は「戻り値の型」ではなく「範囲チェックの幅」 です。戻り値は常に int64(ParseUint は uint64、ParseFloat は float64)なので、小さい型で使うときは自分で型変換します。strconv の公式ドキュメントにも、Atoi は ParseInt(s, 10, 0) を int に変換したもの、Itoa は FormatInt(int64(i), 10) と同じと書かれています。

package main

import (
    "errors"
    "fmt"
    "strconv"
)

func show(label string, v any, err error) {
    fmt.Printf("%-34s 値=%v err=%v\n", label, v, err)
}

func main() {
    v1, err := strconv.ParseInt("127", 10, 8)
    show(`ParseInt("127", 10, 8)`, v1, err)
    v1, err = strconv.ParseInt("128", 10, 8)
    show(`ParseInt("128", 10, 8)`, v1, err)
    v1, err = strconv.ParseInt("3000000000", 10, 32)
    show(`ParseInt("3000000000", 10, 32)`, v1, err)
    v1, err = strconv.ParseInt("3000000000", 10, 0)
    show(`ParseInt("3000000000", 10, 0)`, v1, err)
    v2, err := strconv.ParseUint("-1", 10, 64)
    show(`ParseUint("-1", 10, 64)`, v2, err)
    v2, err = strconv.ParseUint("256", 10, 8)
    show(`ParseUint("256", 10, 8)`, v2, err)

    fmt.Println("---- base ----")
    v1, err = strconv.ParseInt("ff", 16, 64)
    show(`ParseInt("ff", 16, 64)`, v1, err)
    v1, err = strconv.ParseInt("0xff", 16, 64)
    show(`ParseInt("0xff", 16, 64)`, v1, err)
    v1, err = strconv.ParseInt("0xff", 0, 64)
    show(`ParseInt("0xff", 0, 64)`, v1, err)
    v1, err = strconv.ParseInt("0b1010", 0, 64)
    show(`ParseInt("0b1010", 0, 64)`, v1, err)
    v1, err = strconv.ParseInt("010", 0, 64)
    show(`ParseInt("010", 0, 64)`, v1, err)
    v1, err = strconv.ParseInt("010", 10, 64)
    show(`ParseInt("010", 10, 64)`, v1, err)
    v1, err = strconv.ParseInt("1_000", 0, 64)
    show(`ParseInt("1_000", 0, 64)`, v1, err)
    v1, err = strconv.ParseInt("1010", 2, 64)
    show(`ParseInt("1010", 2, 64)`, v1, err)
    v1, err = strconv.ParseInt("z", 36, 64)
    show(`ParseInt("z", 36, 64)`, v1, err)
    v1, err = strconv.ParseInt("10", 37, 64)
    show(`ParseInt("10", 37, 64)`, v1, err)
    v1, err = strconv.ParseInt("10", 10, 65)
    show(`ParseInt("10", 10, 65)`, v1, err)

    fmt.Println("---- FormatInt ----")
    for _, base := range []int{2, 8, 10, 16, 36} {
        fmt.Printf("FormatInt(255, %2d) = %q\n", base, strconv.FormatInt(255, base))
    }
    fmt.Printf("FormatInt(-255, 16) = %q\n", strconv.FormatInt(-255, 16))

    fmt.Println("---- float ----")
    f, err := strconv.ParseFloat("1e400", 64)
    show(`ParseFloat("1e400", 64)`, f, err)
    f, err = strconv.ParseFloat("abc", 64)
    show(`ParseFloat("abc", 64)`, f, err)
    f, err = strconv.ParseFloat("1,234.5", 64)
    show(`ParseFloat("1,234.5", 64)`, f, err)
    f, err = strconv.ParseFloat("NaN", 64)
    show(`ParseFloat("NaN", 64)`, f, err)
    f, err = strconv.ParseFloat("-Inf", 64)
    show(`ParseFloat("-Inf", 64)`, f, err)
    f, err = strconv.ParseFloat("1e3", 64)
    show(`ParseFloat("1e3", 64)`, f, err)
    f, err = strconv.ParseFloat("0.1", 32)
    show(`ParseFloat("0.1", 32)`, f, err)
    fmt.Println("float32(f) =", float32(f))
    f, err = strconv.ParseFloat("3.5e38", 32)
    show(`ParseFloat("3.5e38", 32)`, f, err)

    fmt.Println("---- bool ----")
    for _, s := range []string{"1", "t", "TRUE", "True", "0", "false", "yes", "on", "tRUE", ""} {
        b, err := strconv.ParseBool(s)
        fmt.Printf("ParseBool(%q) = %v, %v\n", s, b, err)
    }

    fmt.Println("---- NumError ----")
    _, err = strconv.ParseInt("12a", 10, 64)
    var ne *strconv.NumError
    if errors.As(err, &ne) {
        fmt.Printf("Func=%q Num=%q Err=%v\n", ne.Func, ne.Num, ne.Err)
    }
    fmt.Println(err == strconv.ErrSyntax, errors.Is(err, strconv.ErrSyntax))
}

実行結果:

ParseInt("127", 10, 8)             値=127 err=<nil>
ParseInt("128", 10, 8)             値=127 err=strconv.ParseInt: parsing "128": value out of range
ParseInt("3000000000", 10, 32)     値=2147483647 err=strconv.ParseInt: parsing "3000000000": value out of range
ParseInt("3000000000", 10, 0)      値=3000000000 err=<nil>
ParseUint("-1", 10, 64)            値=0 err=strconv.ParseUint: parsing "-1": invalid syntax
ParseUint("256", 10, 8)            値=255 err=strconv.ParseUint: parsing "256": value out of range
---- base ----
ParseInt("ff", 16, 64)             値=255 err=<nil>
ParseInt("0xff", 16, 64)           値=0 err=strconv.ParseInt: parsing "0xff": invalid syntax
ParseInt("0xff", 0, 64)            値=255 err=<nil>
ParseInt("0b1010", 0, 64)          値=10 err=<nil>
ParseInt("010", 0, 64)             値=8 err=<nil>
ParseInt("010", 10, 64)            値=10 err=<nil>
ParseInt("1_000", 0, 64)           値=1000 err=<nil>
ParseInt("1010", 2, 64)            値=10 err=<nil>
ParseInt("z", 36, 64)              値=35 err=<nil>
ParseInt("10", 37, 64)             値=0 err=strconv.ParseInt: parsing "10": invalid base 37
ParseInt("10", 10, 65)             値=0 err=strconv.ParseInt: parsing "10": invalid bit size 65
---- FormatInt ----
FormatInt(255,  2) = "11111111"
FormatInt(255,  8) = "377"
FormatInt(255, 10) = "255"
FormatInt(255, 16) = "ff"
FormatInt(255, 36) = "73"
FormatInt(-255, 16) = "-ff"
---- float ----
ParseFloat("1e400", 64)            値=+Inf err=strconv.ParseFloat: parsing "1e400": value out of range
ParseFloat("abc", 64)              値=0 err=strconv.ParseFloat: parsing "abc": invalid syntax
ParseFloat("1,234.5", 64)          値=0 err=strconv.ParseFloat: parsing "1,234.5": invalid syntax
ParseFloat("NaN", 64)              値=NaN err=<nil>
ParseFloat("-Inf", 64)             値=-Inf err=<nil>
ParseFloat("1e3", 64)              値=1000 err=<nil>
ParseFloat("0.1", 32)              値=0.10000000149011612 err=<nil>
float32(f) = 0.1
ParseFloat("3.5e38", 32)           値=+Inf err=strconv.ParseFloat: parsing "3.5e38": value out of range
---- bool ----
ParseBool("1") = true, <nil>
ParseBool("t") = true, <nil>
ParseBool("TRUE") = true, <nil>
ParseBool("True") = true, <nil>
ParseBool("0") = false, <nil>
ParseBool("false") = false, <nil>
ParseBool("yes") = false, strconv.ParseBool: parsing "yes": invalid syntax
ParseBool("on") = false, strconv.ParseBool: parsing "on": invalid syntax
ParseBool("tRUE") = false, strconv.ParseBool: parsing "tRUE": invalid syntax
ParseBool("") = false, strconv.ParseBool: parsing "": invalid syntax
---- NumError ----
Func="ParseInt" Num="12a" Err=invalid syntax
false true

出力から読み取れること

確認した内容 結果
bitSize 8 で 128 を読む 127(int8 の最大値)と value out of range
bitSize 32 で 30 億を読む 2147483647 と value out of range
bitSize 0 で 30 億を読む そのまま読める(この環境の int は 64 ビット)
ParseUint にマイナスの値 invalid syntax(範囲ではなく書式のエラー)
base 16 で "0xff" invalid syntax(base を明示したら接頭辞は付けない)
base 0 で "0xff"、"0b1010"、"1_000" 255、10、1000(Go のリテラルと同じ書き方が通る)
base 0 で "010" 8(先頭の 0 は 8 進数として読まれる)
base 10 で "010" 10
base 37、bitSize 65 invalid base 37、invalid bit size 65
ParseFloat で "1e400" +Inf と value out of range
ParseFloat で "NaN"、"-Inf" エラーにならず NaN、-Inf として読める
ParseFloat("0.1", 32) 戻り値は float64 の 0.10000000149011612。float32 に変換すると 0.1
ParseBool が受け付ける値 1、t、T、TRUE、true、True、0、f、F、FALSE、false、False(出力ではこのうち 6 つを確認)
ParseBool で "yes"、"on"、"tRUE" invalid syntax
  • 利用者が入力した数値を読むときは base 10 を明示する のが安全です。base 0 だと「010」が 8 になります。設定ファイルで 16 進や 2 進も書けるようにしたい、という場面では base 0 が便利です。
  • ParseFloat は "NaN" や "Inf" を数値として受け付けます。受け付けたくない場合は、変換後に math.IsNaN(f) と math.IsInf(f, 0) で確かめます。
  • "yes" や "on" を真として扱いたいときは、ParseBool の前に自分で対応表を持ちます。
  • FormatInt の base も 2〜36 で、16 進は小文字(ff)で返ります。大文字にしたいときは strings.ToUpper を通すか fmt.Sprintf("%X", n) を使います。

string(65) が "A" になる罠と go vet の警告

他の言語の感覚で string(n) と書くと、数字の文字列ではなく その番号の文字(Unicode コードポイント)1 文字 になります。

package main

import (
    "fmt"
    "strconv"
)

func main() {
    n := 65
    s := string(n)
    fmt.Printf("string(65)        = %q\n", s)
    fmt.Printf("string(rune(65))  = %q\n", string(rune(n)))
    fmt.Printf("strconv.Itoa(65)  = %q\n", strconv.Itoa(n))
    fmt.Printf("string(12354)     = %q\n", string(rune(12354)))
    fmt.Printf("string(-1)        = %q\n", string(rune(-1)))
    fmt.Printf("fmt.Sprint(65)    = %q\n", fmt.Sprint(n))
}

実行結果:

string(65)        = "A"
string(rune(65))  = "A"
strconv.Itoa(65)  = "65"
string(12354)     = "あ"
string(-1)        = "�"
fmt.Sprint(65)    = "65"

このコードは go build も go run も通ります。一方で go vet を実行すると、次のように教えてくれます。

# strint
# [strint]
./main.go:10:7: conversion from int to string yields a string of one rune, not a string of digits

「int から string への変換は 1 文字の文字列になり、数字の並びにはなりません」 という意味です。同じフォルダにテストファイルを置いて go test を実行したときも、同じメッセージが出てビルドが止まることを確認しました。go test は実行前に vet の一部を自動でかけるためです。

やりたいこと 書き方 結果
65 を "65" にしたい strconv.Itoa(65) "65"
文字コード 65 を文字にしたい string(rune(65)) "A"
意図があいまいな書き方 string(65) "A"(go vet が知らせる)

文字コードから文字を作りたいときは、string(rune(n)) と rune を経由して書けば、意図が伝わり go vet も通ります。範囲外の値(-1 など)は置換文字(U+FFFD)になります。

fmt.Sprint・Sprintf と strconv の使い分け

数値を文字列にする方法は、strconv のほかに fmt.Sprint と fmt.Sprintf があります。出力と速度の両方を比べました。

package main

import (
    "fmt"
    "strconv"
    "testing"
)

func main() {
    // ---- fmt と strconv の出力の違い ----
    fmt.Printf("%q %q\n", fmt.Sprint(42), strconv.Itoa(42))
    fmt.Printf("%q %q\n", fmt.Sprint(3.0), strconv.FormatFloat(3.0, 'f', -1, 64))
    fmt.Printf("%q %q\n", fmt.Sprint(1234567.0), strconv.FormatFloat(1234567.0, 'f', -1, 64))
    fmt.Printf("%q %q\n", fmt.Sprint(123456789012345678901.0), strconv.FormatFloat(123456789012345678901.0, 'f', -1, 64))
    fmt.Printf("%q %q\n", fmt.Sprint(0.000001234), strconv.FormatFloat(0.000001234, 'f', -1, 64))
    fmt.Printf("%q\n", fmt.Sprintf("%05d|%5d|%-5d|%x|%08.3f", 42, 42, 42, 255, 3.14159))

    fmt.Println("---- FormatFloat ----")
    x := 1234.5678
    fmt.Println(strconv.FormatFloat(x, 'f', 2, 64))
    fmt.Println(strconv.FormatFloat(x, 'f', 0, 64))
    fmt.Println(strconv.FormatFloat(x, 'f', -1, 64))
    fmt.Println(strconv.FormatFloat(x, 'e', 3, 64))
    fmt.Println(strconv.FormatFloat(x, 'g', 3, 64))
    fmt.Println(strconv.FormatFloat(x, 'g', -1, 64))
    fmt.Println(strconv.FormatFloat(0.1+0.2, 'f', -1, 64))
    fmt.Println(strconv.FormatFloat(0.1+0.2, 'f', 2, 64))
    fmt.Println(strconv.FormatFloat(2.5, 'f', 0, 64), strconv.FormatFloat(3.5, 'f', 0, 64), strconv.FormatFloat(2.675, 'f', 2, 64))
    fmt.Println(strconv.FormatFloat(float64(float32(0.1)), 'f', -1, 64), strconv.FormatFloat(float64(float32(0.1)), 'f', -1, 32))

    fmt.Println("---- Quote / Append ----")
    fmt.Println(strconv.Quote("a\tb\"c"))
    u, err := strconv.Unquote(`"a\tb"`)
    fmt.Printf("%q %v\n", u, err)
    buf := []byte("id=")
    buf = strconv.AppendInt(buf, 42, 10)
    buf = append(buf, " ok="...)
    buf = strconv.AppendBool(buf, true)
    fmt.Println(string(buf))

    fmt.Println("---- 速度の目安 ----")
    bench := func(name string, f func()) {
        r := testing.Benchmark(func(b *testing.B) {
            b.ReportAllocs()
            for i := 0; i < b.N; i++ {
                f()
            }
        })
        fmt.Printf("%-22s %6.1f ns/op %3d B/op %d allocs/op\n", name, float64(r.T.Nanoseconds())/float64(r.N), r.AllocedBytesPerOp(), r.AllocsPerOp())
    }
    var sink string
    var isink int
    n := 1234567
    bench("strconv.Itoa", func() { sink = strconv.Itoa(n) })
    bench("strconv.FormatInt", func() { sink = strconv.FormatInt(int64(n), 10) })
    bench("fmt.Sprint", func() { sink = fmt.Sprint(n) })
    bench("fmt.Sprintf(%d)", func() { sink = fmt.Sprintf("%d", n) })
    bench("strconv.Atoi", func() { isink, _ = strconv.Atoi("1234567") })
    bench("fmt.Sscan", func() { fmt.Sscan("1234567", &isink) })
    _, _ = sink, isink
}

実行結果(速度は 2 vCPU・Intel Xeon 2.10GHz のコンテナで測った目安で、環境によって変わります):

"42" "42"
"3" "3"
"1.234567e+06" "1234567"
"1.2345678901234568e+20" "123456789012345680000"
"1.234e-06" "0.000001234"
"00042|   42|42   |ff|0003.142"
---- FormatFloat ----
1234.57
1235
1234.5678
1.235e+03
1.23e+03
1234.5678
0.3
0.30
2 4 2.67
0.10000000149011612 0.1
---- Quote / Append ----
"a\tb\"c"
"a\tb" <nil>
id=42 ok=true
---- 速度の目安 ----
strconv.Itoa             38.2 ns/op   8 B/op 1 allocs/op
strconv.FormatInt        33.5 ns/op   8 B/op 1 allocs/op
fmt.Sprint              105.6 ns/op  16 B/op 2 allocs/op
fmt.Sprintf(%d)         116.9 ns/op  16 B/op 2 allocs/op
strconv.Atoi             10.3 ns/op   0 B/op 0 allocs/op
fmt.Sscan               583.6 ns/op  72 B/op 3 allocs/op

出力の違い

  • int は fmt.Sprint でも strconv.Itoa でも同じ "42" です。
  • float64 は違いが出ます。 fmt.Sprint(1234567.0) は "1.234567e+06" と指数表記になり、FormatFloat(x, 'f', -1, 64) は "1234567" になります。画面や CSV に出す数値は、FormatFloat の 'f' か Sprintf の %f / %.2f を使うと指数表記になりません。
  • ゼロ埋めや桁そろえは Sprintf の担当です(%05d、%5d、%-5d、%x、%08.3f)。

FormatFloat の引数

strconv.FormatFloat(f, fmt, prec, bitSize) の fmt と prec の組み合わせは、次のとおりです(x = 1234.5678 の出力)。

fmt prec 出力 意味
'f' 2 1234.57 小数 2 桁に丸める
'f' 0 1235 整数に丸める
'f' -1 1234.5678 元の値に戻せる最短の桁数
'e' 3 1.235e+03 指数表記
'g' 3 1.23e+03 有効 3 桁(大きさで表記が切り替わる)
'g' -1 1234.5678 最短の表記
  • 0.1 + 0.2 は 'f' と -1 の組み合わせで 0.30000000000000004 になります。小数の桁数を決めて表示する場合は prec に桁数を渡します('f', 2 で 0.30)。
  • 丸めは 2 進数で持っている実際の値に対して行われます。2.5 は 2、3.5 は 4、2.675 は 2.67 になりました。ちょうど中間の値は偶数側に寄り、2.675 は内部ではわずかに小さい値として持たれているためです。厳密な丸めが必要な数量は、整数(最小単位)で持つ設計にすると扱いやすくなります。
  • float32 由来の値を表示するときは bitSize に 32 を渡すと、0.10000000149011612 ではなく 0.1 と表示されます。

速度の目安

書き方 1 回あたり メモリ確保
strconv.Itoa(n) 約 32 ns 1 回
strconv.FormatInt(int64(n), 10) 約 35 ns 1 回
fmt.Sprint(n) 約 109 ns 2 回
fmt.Sprintf("%d", n) 約 130 ns 2 回
strconv.Atoi(s) 約 17 ns 0 回
fmt.Sscan(s, &n) 約 842 ns 3 回

1 回ずつの差はごく小さいので、通常の処理ではどちらを使っても体感は変わりません。大量の行を変換するループでは strconv が向いています。バイト列に直接書き足す strconv.AppendInt / AppendBool を使うと、途中の文字列を作らずに済みます(出力の id=42 ok=true)。

どれを選ぶかの判断表

状況 選ぶもの 理由
int を文字列にするだけ strconv.Itoa 短く、型がはっきりする
int64 / uint64 を文字列に strconv.FormatInt / FormatUint 型変換なしで渡せる
小数の桁数を決めて出す strconv.FormatFloat か fmt.Sprintf("%.2f") 指数表記にならない
文章の中に数値を埋め込む fmt.Sprintf 書式をまとめて書ける
ゼロ埋め・桁そろえ fmt.Sprintf %05d などが使える
型がわからない値(any) fmt.Sprint どの型でも受け取れる
文字列を int にする strconv.Atoi 10 進の int ならこれだけで足りる
範囲を型に合わせて確かめたい strconv.ParseInt(s, 10, 32) など bitSize で範囲を確かめられる
16 進・2 進を読む strconv.ParseInt(s, 16, 64) base を指定できる

変換コードのチェックリスト

確認すること 見るポイント
err を確かめているか 値より先に if err != nil を書いているか
入力を整えているか TrimSpace、全角数字、桁区切りの扱いを決めているか
エラーの見分け方 == ではなく errors.Is を使っているか
型の範囲 int32 などに入れる前に bitSize で確かめているか
base 人が入力する値は 10 を明示しているか
string(n) 数字にしたいなら Itoa、文字にしたいなら string(rune(n)) になっているか
小数の表示 指数表記にならない書式('f')を選んでいるか
go vet CI や保存時に go vet を通しているか

よくある質問

Q. Atoi と ParseInt はどう使い分けますか? A. 10 進の文字列を int にするだけなら Atoi で足ります。int64 で受けたい、16 進を読みたい、int32 の範囲に収まるかを確かめたい、という場合は ParseInt を使います。公式ドキュメントでは、Atoi は ParseInt(s, 10, 0) を int に変換したものと説明されています。

Q. 空文字を 0 として扱いたいときはどうしますか? A. Atoi("") はエラーになるので、先に if s == "" で分けるか、この記事の AtoiOr のように既定値を返す関数を用意します。「未入力」と「0 と入力した」を区別したい場面では、エラーのまま扱うほうが意図を保てます。

Q. "1,234" や "123" を読むにはどうしますか? A. strconv はどちらも ErrSyntax を返します。strings.Map で桁区切りを取り除き、全角数字を半角に置き換えてから Atoi に渡します(この記事の ToInt)。

Q. string(n) はコンパイルエラーにならないのですか? A. なりません。go build は通り、go vet と go test のときに「conversion from int to string yields a string of one rune, not a string of digits」と表示されます。数字にしたいなら strconv.Itoa、文字にしたいなら string(rune(n)) と書きます。

まとめ

  • 文字列から数値は Atoi / ParseInt / ParseUint / ParseFloat / ParseBool、数値から文字列は Itoa / FormatInt / FormatFloat / FormatBool が基本です。
  • 空文字・全角数字・前後の空白・小数点・桁区切りはすべて ErrSyntax、範囲を超えると ErrRange になり、後者は上限の値とエラーが一緒に返ります。
  • エラーの判別は errors.Is(err, strconv.ErrSyntax) / errors.Is(err, strconv.ErrRange)。 == では見分けられません。
  • bitSize は戻り値の型ではなく範囲チェックの幅。 戻り値は int64 / uint64 / float64 のままなので、使う型に変換します。
  • base 0 は 0x・0b・0o・アンダースコアを読める 一方で、"010" が 8 になります。人が入力する値は base 10 を明示します。
  • string(65) は "A"。 数字にするなら strconv.Itoa、go vet を通すと気づけます。
  • 表示用の小数は FormatFloat の 'f' か Sprintf の %.2f、文章への埋め込みやゼロ埋めは Sprintf が向いています。

参考・出典

  • strconv パッケージ(Go 公式ドキュメント): https://pkg.go.dev/strconv

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Goの文字列⇔数値変換|strconv Atoi・ParseInt早見表」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?