Updated:

17 minute read

개요

Go의 strconv 패키지는 기본 타입과 문자열 간의 변환을 제공합니다.

주요 특징:

  • 정수 변환: Atoi, Itoa로 간편한 정수 변환
  • Format/Parse: 모든 기본 타입 양방향 변환
  • 진법 지원: 2진법부터 36진법까지
  • 에러 처리: Parse 함수는 에러 반환
  • Quote/Unquote: 문자열 이스케이프 처리
  • AppendXxx: 버퍼에 직접 추가로 성능 향상
  • 정밀도 제어: 실수 포맷팅 옵션

정수 변환

1. Atoi/Itoa - 간단한 변환

import (
    "fmt"
    "strconv"
)

func main() {
    // Int to ASCII (정수 → 문자열)
    s := strconv.Itoa(123)
    fmt.Printf("%s (type: %T)\n", s, s)  // 123 (type: string)
    
    // ASCII to Int (문자열 → 정수)
    n, err := strconv.Atoi("456")
    if err != nil {
        fmt.Println("에러:", err)
    }
    fmt.Printf("%d (type: %T)\n", n, n)  // 456 (type: int)
    
    // 음수
    s = strconv.Itoa(-789)
    fmt.Println(s)  // -789
    
    n, _ = strconv.Atoi("-999")
    fmt.Println(n)  // -999
}

2. ParseInt - 상세한 정수 파싱

func main() {
    // ParseInt(s string, base int, bitSize int) (int64, error)
    
    // 10진수, 64비트
    n, err := strconv.ParseInt("12345", 10, 64)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(n)  // 12345
    
    // 10진수, 32비트 (범위 초과 시 에러)
    n, err = strconv.ParseInt("2147483648", 10, 32)
    if err != nil {
        fmt.Println(err)  // strconv.ParseInt: parsing "2147483648": value out of range
    }
    
    // 음수
    n, _ = strconv.ParseInt("-999", 10, 64)
    fmt.Println(n)  // -999
    
    // base 0 = 자동 감지 (0x, 0o, 0b 접두사)
    n, _ = strconv.ParseInt("0x1F", 0, 64)
    fmt.Println(n)  // 31
}

3. FormatInt - 정수 포맷팅

func main() {
    // FormatInt(i int64, base int) string
    
    num := int64(255)
    
    // 10진수
    s := strconv.FormatInt(num, 10)
    fmt.Println(s)  // 255
    
    // 2진수
    s = strconv.FormatInt(num, 2)
    fmt.Println(s)  // 11111111
    
    // 16진수
    s = strconv.FormatInt(num, 16)
    fmt.Println(s)  // ff
    
    // 음수
    s = strconv.FormatInt(-123, 10)
    fmt.Println(s)  // -123
}

4. ParseUint/FormatUint - 부호 없는 정수

func main() {
    // 부호 없는 정수 파싱
    n, err := strconv.ParseUint("12345", 10, 64)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(n)  // 12345
    
    // 음수는 에러
    _, err = strconv.ParseUint("-123", 10, 64)
    if err != nil {
        fmt.Println(err)  // strconv.ParseUint: parsing "-123": invalid syntax
    }
    
    // 포맷팅
    s := strconv.FormatUint(255, 16)
    fmt.Println(s)  // ff
}

진법 변환

1. 2진법

func main() {
    num := 42
    
    // 10진수 → 2진수
    binary := strconv.FormatInt(int64(num), 2)
    fmt.Printf("%d = %s (binary)\n", num, binary)
    // 42 = 101010 (binary)
    
    // 2진수 → 10진수
    n, _ := strconv.ParseInt("101010", 2, 64)
    fmt.Println(n)  // 42
}

2. 8진법

func main() {
    num := 64
    
    // 10진수 → 8진수
    octal := strconv.FormatInt(int64(num), 8)
    fmt.Printf("%d = %s (octal)\n", num, octal)
    // 64 = 100 (octal)
    
    // 8진수 → 10진수
    n, _ := strconv.ParseInt("100", 8, 64)
    fmt.Println(n)  // 64
    
    // 0o 접두사 인식
    n, _ = strconv.ParseInt("0o100", 0, 64)
    fmt.Println(n)  // 64
}

3. 16진법

func main() {
    num := 255
    
    // 10진수 → 16진수
    hex := strconv.FormatInt(int64(num), 16)
    fmt.Printf("%d = %s (hex)\n", num, hex)
    // 255 = ff (hex)
    
    // 16진수 → 10진수
    n, _ := strconv.ParseInt("ff", 16, 64)
    fmt.Println(n)  // 255
    
    // 대소문자 모두 인식
    n, _ = strconv.ParseInt("FF", 16, 64)
    fmt.Println(n)  // 255
    
    // 0x 접두사 인식
    n, _ = strconv.ParseInt("0xFF", 0, 64)
    fmt.Println(n)  // 255
}

4. 36진법

func main() {
    num := 1234
    
    // 10진수 → 36진법 (0-9, a-z)
    base36 := strconv.FormatInt(int64(num), 36)
    fmt.Printf("%d = %s (base36)\n", num, base36)
    // 1234 = ya (base36)
    
    // 36진법 → 10진수
    n, _ := strconv.ParseInt("ya", 36, 64)
    fmt.Println(n)  // 1234
}

실수 변환

1. ParseFloat - 실수 파싱

func main() {
    // ParseFloat(s string, bitSize int) (float64, error)
    
    // 기본 실수
    f, err := strconv.ParseFloat("3.14", 64)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(f)  // 3.14
    
    // 과학적 표기법
    f, _ = strconv.ParseFloat("1.23e-4", 64)
    fmt.Println(f)  // 0.000123
    
    // 음수
    f, _ = strconv.ParseFloat("-2.71", 64)
    fmt.Println(f)  // -2.71
    
    // 특수 값
    f, _ = strconv.ParseFloat("Inf", 64)
    fmt.Println(f)  // +Inf
    
    f, _ = strconv.ParseFloat("NaN", 64)
    fmt.Println(f)  // NaN
}

2. FormatFloat - 실수 포맷팅

func main() {
    // FormatFloat(f float64, fmt byte, prec int, bitSize int) string
    
    num := 3.14159265359
    
    // 'f': 고정 소수점
    s := strconv.FormatFloat(num, 'f', 2, 64)
    fmt.Println(s)  // 3.14
    
    s = strconv.FormatFloat(num, 'f', -1, 64)
    fmt.Println(s)  // 3.14159265359
    
    // 'e': 과학적 표기법
    s = strconv.FormatFloat(num, 'e', 2, 64)
    fmt.Println(s)  // 3.14e+00
    
    // 'g': 자동 선택 (간결한 표현)
    s = strconv.FormatFloat(num, 'g', 5, 64)
    fmt.Println(s)  // 3.1416
    
    // 'E': 대문자 E
    s = strconv.FormatFloat(num, 'E', 2, 64)
    fmt.Println(s)  // 3.14E+00
}

3. 포맷 옵션 비교

func main() {
    num := 1234.56789
    
    fmt.Println("'f':", strconv.FormatFloat(num, 'f', 2, 64))  // 1234.57
    fmt.Println("'e':", strconv.FormatFloat(num, 'e', 2, 64))  // 1.23e+03
    fmt.Println("'E':", strconv.FormatFloat(num, 'E', 2, 64))  // 1.23E+03
    fmt.Println("'g':", strconv.FormatFloat(num, 'g', 5, 64))  // 1234.6
    fmt.Println("'G':", strconv.FormatFloat(num, 'G', 5, 64))  // 1234.6
    
    smallNum := 0.00012345
    fmt.Println("'f':", strconv.FormatFloat(smallNum, 'f', 5, 64))  // 0.00012
    fmt.Println("'e':", strconv.FormatFloat(smallNum, 'e', 2, 64))  // 1.23e-04
    fmt.Println("'g':", strconv.FormatFloat(smallNum, 'g', 3, 64))  // 0.000123
}

Bool 변환

1. ParseBool/FormatBool

func main() {
    // FormatBool
    s := strconv.FormatBool(true)
    fmt.Println(s)  // true
    
    s = strconv.FormatBool(false)
    fmt.Println(s)  // false
    
    // ParseBool - 다양한 형식 지원
    b, _ := strconv.ParseBool("true")
    fmt.Println(b)  // true
    
    b, _ = strconv.ParseBool("1")
    fmt.Println(b)  // true
    
    b, _ = strconv.ParseBool("t")
    fmt.Println(b)  // true
    
    b, _ = strconv.ParseBool("T")
    fmt.Println(b)  // true
    
    b, _ = strconv.ParseBool("TRUE")
    fmt.Println(b)  // true
    
    b, _ = strconv.ParseBool("false")
    fmt.Println(b)  // false
    
    b, _ = strconv.ParseBool("0")
    fmt.Println(b)  // false
    
    b, _ = strconv.ParseBool("f")
    fmt.Println(b)  // false
    
    // 잘못된 값
    _, err := strconv.ParseBool("yes")
    if err != nil {
        fmt.Println(err)  // strconv.ParseBool: parsing "yes": invalid syntax
    }
}

Quote/Unquote

1. Quote - 문자열 이스케이프

func main() {
    // 기본 문자열
    s := strconv.Quote("hello")
    fmt.Println(s)  // "hello"
    
    // 특수 문자 포함
    s = strconv.Quote("hello\nworld")
    fmt.Println(s)  // "hello\nworld"
    
    // 탭, 따옴표
    s = strconv.Quote("tab\there\t\"quoted\"")
    fmt.Println(s)  // "tab\there\t\"quoted\""
    
    // 유니코드
    s = strconv.Quote("안녕")
    fmt.Println(s)  // "안녕"
}

2. QuoteToASCII - ASCII만 사용

func main() {
    // 비ASCII 문자를 \uXXXX로 변환
    s := strconv.QuoteToASCII("안녕")
    fmt.Println(s)  // "\uc548\ub155"
    
    s = strconv.QuoteToASCII("Hello 세계")
    fmt.Println(s)  // "Hello \uc138\uacc4"
}

3. Unquote - 이스케이프 해제

func main() {
    // Quote된 문자열 원래대로
    s, err := strconv.Unquote(`"hello\nworld"`)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Printf("%q\n", s)  // "hello\nworld"
    fmt.Println(s)
    // hello
    // world
    
    // 유니코드
    s, _ = strconv.Unquote(`"\uc548\ub155"`)
    fmt.Println(s)  // 안녕
    
    // 잘못된 형식
    _, err = strconv.Unquote("hello")
    if err != nil {
        fmt.Println(err)  // invalid syntax
    }
}

4. QuoteRune/QuoteRuneToASCII

func main() {
    // 룬(문자) Quote
    s := strconv.QuoteRune('A')
    fmt.Println(s)  // 'A'
    
    s = strconv.QuoteRune('\n')
    fmt.Println(s)  // '\n'
    
    // 한글
    s = strconv.QuoteRune('가')
    fmt.Println(s)  // '가'
    
    // ASCII로
    s = strconv.QuoteRuneToASCII('가')
    fmt.Println(s)  // '\uac00'
}

Append 함수

1. AppendInt - 버퍼에 직접 추가

func main() {
    // 기존 버퍼에 추가 (메모리 할당 최소화)
    buf := make([]byte, 0, 100)
    
    buf = strconv.AppendInt(buf, 123, 10)
    fmt.Println(string(buf))  // 123
    
    buf = append(buf, ' ')
    buf = strconv.AppendInt(buf, 456, 10)
    fmt.Println(string(buf))  // 123 456
    
    buf = append(buf, ' ')
    buf = strconv.AppendInt(buf, 255, 16)
    fmt.Println(string(buf))  // 123 456 ff
}

2. AppendFloat - 실수 추가

func main() {
    buf := make([]byte, 0, 100)
    
    buf = strconv.AppendFloat(buf, 3.14, 'f', 2, 64)
    fmt.Println(string(buf))  // 3.14
    
    buf = append(buf, ' ')
    buf = strconv.AppendFloat(buf, 2.71, 'e', 2, 64)
    fmt.Println(string(buf))  // 3.14 2.71e+00
}

3. AppendBool/AppendQuote

func main() {
    buf := make([]byte, 0, 100)
    
    buf = strconv.AppendBool(buf, true)
    buf = append(buf, ' ')
    buf = strconv.AppendBool(buf, false)
    fmt.Println(string(buf))  // true false
    
    buf = buf[:0]
    buf = strconv.AppendQuote(buf, "hello")
    fmt.Println(string(buf))  // "hello"
}

실전 예제

1. 설정 파일 파싱

import (
    "bufio"
    "os"
    "strings"
)

type Config struct {
    Port       int
    EnableSSL  bool
    Timeout    float64
    MaxClients uint64
}

func ParseConfig(filename string) (*Config, error) {
    file, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    defer file.Close()
    
    cfg := &Config{}
    scanner := bufio.NewScanner(file)
    
    for scanner.Scan() {
        line := strings.TrimSpace(scanner.Text())
        if line == "" || strings.HasPrefix(line, "#") {
            continue
        }
        
        parts := strings.SplitN(line, "=", 2)
        if len(parts) != 2 {
            continue
        }
        
        key := strings.TrimSpace(parts[0])
        value := strings.TrimSpace(parts[1])
        
        switch key {
        case "port":
            if port, err := strconv.Atoi(value); err == nil {
                cfg.Port = port
            }
        case "enable_ssl":
            if ssl, err := strconv.ParseBool(value); err == nil {
                cfg.EnableSSL = ssl
            }
        case "timeout":
            if timeout, err := strconv.ParseFloat(value, 64); err == nil {
                cfg.Timeout = timeout
            }
        case "max_clients":
            if max, err := strconv.ParseUint(value, 10, 64); err == nil {
                cfg.MaxClients = max
            }
        }
    }
    
    return cfg, scanner.Err()
}

func main() {
    // config.txt:
    // port=8080
    // enable_ssl=true
    // timeout=30.5
    // max_clients=1000
    
    cfg, err := ParseConfig("config.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
    
    fmt.Printf("Port: %d\n", cfg.Port)
    fmt.Printf("SSL: %v\n", cfg.EnableSSL)
    fmt.Printf("Timeout: %.1f\n", cfg.Timeout)
    fmt.Printf("Max Clients: %d\n", cfg.MaxClients)
}

2. CSV 파서

import (
    "encoding/csv"
    "io"
)

type Person struct {
    Name string
    Age  int
    GPA  float64
}

func ParseCSV(r io.Reader) ([]Person, error) {
    csvReader := csv.NewReader(r)
    
    // 헤더 스킵
    if _, err := csvReader.Read(); err != nil {
        return nil, err
    }
    
    var people []Person
    
    for {
        record, err := csvReader.Read()
        if err == io.EOF {
            break
        }
        if err != nil {
            return nil, err
        }
        
        if len(record) < 3 {
            continue
        }
        
        age, err := strconv.Atoi(record[1])
        if err != nil {
            continue
        }
        
        gpa, err := strconv.ParseFloat(record[2], 64)
        if err != nil {
            continue
        }
        
        people = append(people, Person{
            Name: record[0],
            Age:  age,
            GPA:  gpa,
        })
    }
    
    return people, nil
}

func main() {
    data := `Name,Age,GPA
Alice,25,3.8
Bob,22,3.5
Charlie,24,3.9`
    
    people, err := ParseCSV(strings.NewReader(data))
    if err != nil {
        fmt.Println(err)
        return
    }
    
    for _, p := range people {
        fmt.Printf("%s: %d세, GPA %.1f\n", p.Name, p.Age, p.GPA)
    }
}

3. 진법 변환기

type BaseConverter struct{}

func (bc BaseConverter) Convert(num string, fromBase, toBase int) (string, error) {
    // 문자열을 10진수로 변환
    decimal, err := strconv.ParseInt(num, fromBase, 64)
    if err != nil {
        return "", fmt.Errorf("변환 실패: %w", err)
    }
    
    // 10진수를 목표 진법으로 변환
    result := strconv.FormatInt(decimal, toBase)
    return result, nil
}

func main() {
    bc := BaseConverter{}
    
    // 2진수 → 16진수
    result, _ := bc.Convert("11111111", 2, 16)
    fmt.Printf("11111111 (2진수) = %s (16진수)\n", result)  // ff
    
    // 16진수 → 10진수
    result, _ = bc.Convert("ff", 16, 10)
    fmt.Printf("ff (16진수) = %s (10진수)\n", result)  // 255
    
    // 10진수 → 2진수
    result, _ = bc.Convert("42", 10, 2)
    fmt.Printf("42 (10진수) = %s (2진수)\n", result)  // 101010
}

4. 파일 크기 포맷터

func FormatFileSize(bytes int64) string {
    const unit = 1024
    
    if bytes < unit {
        return strconv.FormatInt(bytes, 10) + " B"
    }
    
    div, exp := int64(unit), 0
    for n := bytes / unit; n >= unit; n /= unit {
        div *= unit
        exp++
    }
    
    units := []string{"KB", "MB", "GB", "TB", "PB"}
    value := float64(bytes) / float64(div)
    
    return strconv.FormatFloat(value, 'f', 1, 64) + " " + units[exp]
}

func main() {
    fmt.Println(FormatFileSize(500))           // 500 B
    fmt.Println(FormatFileSize(1024))          // 1.0 KB
    fmt.Println(FormatFileSize(1536))          // 1.5 KB
    fmt.Println(FormatFileSize(1048576))       // 1.0 MB
    fmt.Println(FormatFileSize(1073741824))    // 1.0 GB
    fmt.Println(FormatFileSize(5368709120))    // 5.0 GB
}

5. URL 쿼리 파라미터 파싱

import "net/url"

type QueryParams struct {
    Page     int
    PageSize int
    SortBy   string
    Desc     bool
}

func ParseQuery(query string) (*QueryParams, error) {
    values, err := url.ParseQuery(query)
    if err != nil {
        return nil, err
    }
    
    params := &QueryParams{
        Page:     1,
        PageSize: 10,
        SortBy:   "id",
        Desc:     false,
    }
    
    if page := values.Get("page"); page != "" {
        if p, err := strconv.Atoi(page); err == nil && p > 0 {
            params.Page = p
        }
    }
    
    if size := values.Get("page_size"); size != "" {
        if s, err := strconv.Atoi(size); err == nil && s > 0 {
            params.PageSize = s
        }
    }
    
    if sort := values.Get("sort_by"); sort != "" {
        params.SortBy = sort
    }
    
    if desc := values.Get("desc"); desc != "" {
        if d, err := strconv.ParseBool(desc); err == nil {
            params.Desc = d
        }
    }
    
    return params, nil
}

func main() {
    query := "page=2&page_size=20&sort_by=name&desc=true"
    
    params, err := ParseQuery(query)
    if err != nil {
        fmt.Println(err)
        return
    }
    
    fmt.Printf("Page: %d\n", params.Page)
    fmt.Printf("PageSize: %d\n", params.PageSize)
    fmt.Printf("SortBy: %s\n", params.SortBy)
    fmt.Printf("Desc: %v\n", params.Desc)
}

6. JSON 빌더 (간단 버전)

import "bytes"

type JSONBuilder struct {
    buf bytes.Buffer
}

func (jb *JSONBuilder) StartObject() {
    jb.buf.WriteByte('{')
}

func (jb *JSONBuilder) EndObject() {
    if jb.buf.Len() > 0 && jb.buf.Bytes()[jb.buf.Len()-1] == ',' {
        jb.buf.Truncate(jb.buf.Len() - 1)
    }
    jb.buf.WriteByte('}')
}

func (jb *JSONBuilder) AddString(key, value string) {
    jb.buf.WriteString(strconv.Quote(key))
    jb.buf.WriteByte(':')
    jb.buf.WriteString(strconv.Quote(value))
    jb.buf.WriteByte(',')
}

func (jb *JSONBuilder) AddInt(key string, value int) {
    jb.buf.WriteString(strconv.Quote(key))
    jb.buf.WriteByte(':')
    jb.buf.WriteString(strconv.Itoa(value))
    jb.buf.WriteByte(',')
}

func (jb *JSONBuilder) AddFloat(key string, value float64) {
    jb.buf.WriteString(strconv.Quote(key))
    jb.buf.WriteByte(':')
    jb.buf.WriteString(strconv.FormatFloat(value, 'f', 2, 64))
    jb.buf.WriteByte(',')
}

func (jb *JSONBuilder) AddBool(key string, value bool) {
    jb.buf.WriteString(strconv.Quote(key))
    jb.buf.WriteByte(':')
    jb.buf.WriteString(strconv.FormatBool(value))
    jb.buf.WriteByte(',')
}

func (jb *JSONBuilder) String() string {
    return jb.buf.String()
}

func main() {
    jb := &JSONBuilder{}
    
    jb.StartObject()
    jb.AddString("name", "Alice")
    jb.AddInt("age", 30)
    jb.AddFloat("score", 95.5)
    jb.AddBool("active", true)
    jb.EndObject()
    
    fmt.Println(jb.String())
    // {"name":"Alice","age":30,"score":95.50,"active":true}
}

7. 로그 레벨 파서

type LogLevel int

const (
    DEBUG LogLevel = iota
    INFO
    WARN
    ERROR
)

func (l LogLevel) String() string {
    switch l {
    case DEBUG:
        return "DEBUG"
    case INFO:
        return "INFO"
    case WARN:
        return "WARN"
    case ERROR:
        return "ERROR"
    default:
        return "UNKNOWN"
    }
}

func ParseLogLevel(s string) (LogLevel, error) {
    switch strings.ToUpper(s) {
    case "DEBUG", "0":
        return DEBUG, nil
    case "INFO", "1":
        return INFO, nil
    case "WARN", "2":
        return WARN, nil
    case "ERROR", "3":
        return ERROR, nil
    default:
        // 숫자로 시도
        if level, err := strconv.Atoi(s); err == nil {
            if level >= 0 && level <= 3 {
                return LogLevel(level), nil
            }
        }
        return 0, fmt.Errorf("invalid log level: %s", s)
    }
}

func main() {
    levels := []string{"DEBUG", "1", "warn", "3", "unknown"}
    
    for _, s := range levels {
        if level, err := ParseLogLevel(s); err == nil {
            fmt.Printf("%s → %s\n", s, level)
        } else {
            fmt.Printf("%s → %v\n", s, err)
        }
    }
    // DEBUG → DEBUG
    // 1 → INFO
    // warn → WARN
    // 3 → ERROR
    // unknown → invalid log level: unknown
}

8. 성능 최적화 버퍼

import (
    "bytes"
    "time"
)

type LogBuffer struct {
    buf bytes.Buffer
}

func (lb *LogBuffer) Log(level string, msg string) {
    // Append 함수로 메모리 할당 최소화
    lb.buf.WriteByte('[')
    lb.buf = *bytes.NewBuffer(
        strconv.AppendInt(lb.buf.Bytes(), time.Now().Unix(), 10),
    )
    lb.buf.WriteString("] [")
    lb.buf.WriteString(level)
    lb.buf.WriteString("] ")
    lb.buf.WriteString(msg)
    lb.buf.WriteByte('\n')
}

func (lb *LogBuffer) String() string {
    return lb.buf.String()
}

func (lb *LogBuffer) Clear() {
    lb.buf.Reset()
}

func main() {
    logger := &LogBuffer{}
    
    logger.Log("INFO", "Application started")
    logger.Log("DEBUG", "Database connected")
    logger.Log("ERROR", "Failed to load config")
    
    fmt.Print(logger.String())
    // [1234567890] [INFO] Application started
    // [1234567890] [DEBUG] Database connected
    // [1234567890] [ERROR] Failed to load config
}

일반적인 실수

1. 에러 무시

// ❌ 나쁜 예 (에러 무시)
func main() {
    n, _ := strconv.Atoi("abc")
    fmt.Println(n + 10)  // 0 + 10 = 10 (잘못된 결과)
}

// ✅ 좋은 예 (에러 처리)
func main() {
    n, err := strconv.Atoi("abc")
    if err != nil {
        fmt.Println("변환 실패:", err)
        return
    }
    fmt.Println(n + 10)
}

2. 범위 초과 무시

// ❌ 나쁜 예 (범위 체크 안 함)
func main() {
    s := "2147483648"  // int32 범위 초과
    n, _ := strconv.ParseInt(s, 10, 32)
    fmt.Println(n)  // 에러 무시
}

// ✅ 좋은 예 (범위 확인)
func main() {
    s := "2147483648"
    n, err := strconv.ParseInt(s, 10, 32)
    if err != nil {
        fmt.Println("범위 초과:", err)
        return
    }
    fmt.Println(n)
}

3. 진법 혼동

// ❌ 나쁜 예 (진법 지정 안 함)
func main() {
    // "010"을 10진수로 해석
    n, _ := strconv.Atoi("010")
    fmt.Println(n)  // 10 (8진수 아님!)
}

// ✅ 좋은 예 (진법 명시)
func main() {
    // 8진수 파싱
    n, _ := strconv.ParseInt("010", 8, 64)
    fmt.Println(n)  // 8
    
    // 자동 감지
    n, _ = strconv.ParseInt("0o10", 0, 64)
    fmt.Println(n)  // 8
}

4. FormatFloat 정밀도 오해

// ❌ 나쁜 예 (정밀도 의미 오해)
func main() {
    f := 3.14159
    
    // prec는 전체 자릿수가 아님
    s := strconv.FormatFloat(f, 'f', 10, 64)
    fmt.Println(s)  // 3.1415900000 (소수점 이하 10자리)
}

// ✅ 좋은 예 (정확한 이해)
func main() {
    f := 3.14159
    
    // 'f': prec는 소수점 이하 자릿수
    s := strconv.FormatFloat(f, 'f', 2, 64)
    fmt.Println(s)  // 3.14
    
    // 'g': prec는 유효 숫자
    s = strconv.FormatFloat(f, 'g', 3, 64)
    fmt.Println(s)  // 3.14
}

5. Quote/Unquote 짝 안 맞음

// ❌ 나쁜 예 (Quote 없이 Unquote)
func main() {
    s := "hello\nworld"
    unquoted, err := strconv.Unquote(s)
    if err != nil {
        fmt.Println(err)  // invalid syntax
    }
}

// ✅ 좋은 예 (Quote 후 Unquote)
func main() {
    original := "hello\nworld"
    quoted := strconv.Quote(original)
    
    unquoted, err := strconv.Unquote(quoted)
    if err != nil {
        fmt.Println(err)
        return
    }
    
    fmt.Println(original == unquoted)  // true
}

6. bitSize 혼동

// ❌ 나쁜 예 (bitSize 의미 오해)
func main() {
    // bitSize는 결과 타입 크기 (32비트 int로 변환하려고)
    n, _ := strconv.ParseInt("100", 10, 32)
    
    // 하지만 결과는 항상 int64
    var i32 int32 = int32(n)  // 명시적 변환 필요
}

// ✅ 좋은 예 (올바른 사용)
func main() {
    // bitSize는 범위 검증용
    n, err := strconv.ParseInt("2147483648", 10, 32)
    if err != nil {
        fmt.Println("32비트 범위 초과:", err)
        return
    }
    
    i32 := int32(n)  // 안전하게 변환
    fmt.Println(i32)
}

7. Append 함수 반환값 무시

// ❌ 나쁜 예 (반환값 무시)
func main() {
    buf := make([]byte, 0, 10)
    strconv.AppendInt(buf, 123, 10)  // 반환값 사용 안 함
    
    fmt.Println(string(buf))  // "" (빈 문자열)
}

// ✅ 좋은 예 (반환값 사용)
func main() {
    buf := make([]byte, 0, 10)
    buf = strconv.AppendInt(buf, 123, 10)  // 반환값 할당
    
    fmt.Println(string(buf))  // "123"
}

베스트 프랙티스

1. 에러 처리

// ✅ 명확한 에러 메시지
func ParsePort(s string) (int, error) {
    port, err := strconv.Atoi(s)
    if err != nil {
        return 0, fmt.Errorf("포트 번호 변환 실패: %w", err)
    }
    
    if port < 1 || port > 65535 {
        return 0, fmt.Errorf("포트 번호 범위 오류: %d", port)
    }
    
    return port, nil
}

2. 기본값 제공

// ✅ 변환 실패 시 기본값
func ParseIntWithDefault(s string, defaultValue int) int {
    if n, err := strconv.Atoi(s); err == nil {
        return n
    }
    return defaultValue
}

func main() {
    port := ParseIntWithDefault(os.Getenv("PORT"), 8080)
    fmt.Println("Port:", port)
}

3. 타입별 함수 사용

// ✅ 적절한 함수 선택
func main() {
    // 간단한 int 변환: Atoi
    n, _ := strconv.Atoi("123")
    
    // 진법 지정 필요: ParseInt
    hex, _ := strconv.ParseInt("FF", 16, 64)
    
    // 부호 없는 정수: ParseUint
    unsigned, _ := strconv.ParseUint("255", 10, 64)
    
    // 실수: ParseFloat
    f, _ := strconv.ParseFloat("3.14", 64)
    
    fmt.Println(n, hex, unsigned, f)
}

4. Append 함수로 성능 최적화

// ✅ 여러 값 연결 시 Append 사용
func FormatRecord(id int, name string, score float64) string {
    buf := make([]byte, 0, 100)
    
    buf = strconv.AppendInt(buf, int64(id), 10)
    buf = append(buf, ',')
    buf = strconv.AppendQuote(buf, name)
    buf = append(buf, ',')
    buf = strconv.AppendFloat(buf, score, 'f', 2, 64)
    
    return string(buf)
}

func main() {
    fmt.Println(FormatRecord(1, "Alice", 95.5))
    // 1,"Alice",95.50
}

5. 검증 함수

// ✅ 변환 전 검증
func IsValidInt(s string) bool {
    _, err := strconv.Atoi(s)
    return err == nil
}

func IsValidFloat(s string) bool {
    _, err := strconv.ParseFloat(s, 64)
    return err == nil
}

func main() {
    inputs := []string{"123", "abc", "3.14"}
    
    for _, input := range inputs {
        fmt.Printf("%s: int=%v, float=%v\n",
            input,
            IsValidInt(input),
            IsValidFloat(input))
    }
}

6. 안전한 타입 변환

// ✅ 범위 체크 포함
func SafeIntToInt32(n int64) (int32, error) {
    if n < math.MinInt32 || n > math.MaxInt32 {
        return 0, fmt.Errorf("범위 초과: %d", n)
    }
    return int32(n), nil
}

func ParseToInt32(s string) (int32, error) {
    n, err := strconv.ParseInt(s, 10, 32)
    if err != nil {
        return 0, err
    }
    return int32(n), nil
}

7. 문서화

// ✅ 명확한 문서화
// ParseDuration converts a string to duration in seconds.
// Supported formats:
//   - "30" or "30s" for 30 seconds
//   - "5m" for 5 minutes (300 seconds)
//   - "2h" for 2 hours (7200 seconds)
// Returns an error if the format is invalid.
func ParseDuration(s string) (int, error) {
    s = strings.TrimSpace(s)
    
    if strings.HasSuffix(s, "h") {
        hours, err := strconv.Atoi(strings.TrimSuffix(s, "h"))
        if err != nil {
            return 0, err
        }
        return hours * 3600, nil
    }
    
    if strings.HasSuffix(s, "m") {
        minutes, err := strconv.Atoi(strings.TrimSuffix(s, "m"))
        if err != nil {
            return 0, err
        }
        return minutes * 60, nil
    }
    
    return strconv.Atoi(strings.TrimSuffix(s, "s"))
}

8. 테스트

// ✅ 경계값 테스트
func TestParseInt(t *testing.T) {
    tests := []struct {
        input   string
        want    int
        wantErr bool
    }{
        {"0", 0, false},
        {"123", 123, false},
        {"-456", -456, false},
        {"abc", 0, true},
        {"", 0, true},
        {"2147483647", 2147483647, false},  // max int32
    }
    
    for _, tt := range tests {
        t.Run(tt.input, func(t *testing.T) {
            got, err := strconv.Atoi(tt.input)
            
            if (err != nil) != tt.wantErr {
                t.Errorf("wantErr %v, got %v", tt.wantErr, err)
            }
            
            if !tt.wantErr && got != tt.want {
                t.Errorf("want %d, got %d", tt.want, got)
            }
        })
    }
}

정리

  • 정수: Atoi/Itoa (간편), ParseInt/FormatInt (진법 지정), ParseUint/FormatUint (부호 없음)
  • 진법: 2진법(2), 8진법(8), 16진법(16), 최대 36진법, base 0으로 자동 감지
  • 실수: ParseFloat/FormatFloat, fmt 옵션(‘f’, ‘e’, ‘g’), prec로 정밀도 제어
  • Bool: ParseBool (다양한 형식), FormatBool (true/false)
  • Quote: Quote/Unquote (이스케이프), QuoteToASCII (유니코드→ASCII), QuoteRune (문자)
  • Append: AppendInt/Float/Bool/Quote로 버퍼에 직접 추가, 메모리 할당 최소화
  • 실전: 설정 파싱, CSV 파서, 진법 변환기, 파일 크기 포맷터, URL 쿼리, JSON 빌더, 로그 레벨, 성능 버퍼
  • 실수: 에러 무시, 범위 초과, 진법 혼동, 정밀도 오해, Quote/Unquote 짝, bitSize 혼동, Append 반환값
  • 베스트: 에러 처리, 기본값, 적절한 함수, Append 활용, 검증, 안전한 변환, 문서화, 테스트