본문으로 건너뛰기

strings · strconv · unicode · regexp 패키지 — 문자열 처리 완전 정복

Go의 표준 라이브러리는 문자열 처리에 필요한 모든 도구를 제공합니다. strings로 기본 조작, strconv로 타입 변환, unicode로 문자 분류, regexp로 정규 표현식을 처리합니다.

strings 패키지 — 문자열 조작

검색과 확인

package main

import (
"fmt"
"strings"
)

func main() {
s := "Hello, Go World!"

// 포함 여부 확인
fmt.Println(strings.Contains(s, "Go")) // true
fmt.Println(strings.ContainsAny(s, "aeiou")) // true — 문자 중 하나라도 포함
fmt.Println(strings.ContainsRune(s, 'W')) // true — 특정 룬 포함

// 접두어/접미어
fmt.Println(strings.HasPrefix(s, "Hello")) // true
fmt.Println(strings.HasSuffix(s, "World!")) // true

// 개수 세기
fmt.Println(strings.Count(s, "l")) // 3 — 'l' 개수
fmt.Println(strings.Count(s, "o")) // 2 — 'o' 개수

// 인덱스 찾기
fmt.Println(strings.Index(s, "Go")) // 7 — 첫 번째 위치
fmt.Println(strings.LastIndex(s, "o")) // 15 — 마지막 위치
fmt.Println(strings.IndexAny(s, "aeiou")) // 1 — 모음 첫 위치
fmt.Println(strings.IndexRune(s, 'W')) // 11 — 룬 위치
}

변환과 수정

package main

import (
"fmt"
"strings"
)

func main() {
// 대소문자 변환
fmt.Println(strings.ToUpper("hello")) // HELLO
fmt.Println(strings.ToLower("WORLD")) // world
fmt.Println(strings.Title("hello world")) // Hello World (deprecated, 유니코드 미지원)

// 공백 제거
s := " Hello, Go! "
fmt.Println(strings.TrimSpace(s)) // "Hello, Go!"
fmt.Println(strings.Trim(s, " ")) // "Hello, Go!"
fmt.Println(strings.TrimLeft(s, " ")) // "Hello, Go! "
fmt.Println(strings.TrimRight(s, " ")) // " Hello, Go!"
fmt.Println(strings.TrimPrefix("Hello, Go!", "Hello, ")) // "Go!"
fmt.Println(strings.TrimSuffix("Hello, Go!", ", Go!")) // "Hello"

// 교체
text := "foo bar foo baz foo"
fmt.Println(strings.Replace(text, "foo", "qux", 2)) // "qux bar qux baz foo" — 2개만
fmt.Println(strings.ReplaceAll(text, "foo", "qux")) // "qux bar qux baz qux" — 전체

// 반복
fmt.Println(strings.Repeat("Go! ", 3)) // "Go! Go! Go! "

// 분할
csv := "apple,banana,cherry"
parts := strings.Split(csv, ",")
fmt.Println(parts) // [apple banana cherry]
fmt.Println(len(parts)) // 3

// n개로만 분할
fmt.Println(strings.SplitN(csv, ",", 2)) // [apple banana,cherry]

// 공백 기준 분할 (연속 공백 처리)
words := strings.Fields(" foo bar baz ")
fmt.Println(words) // [foo bar baz]

// 합치기
fmt.Println(strings.Join(parts, " | ")) // "apple | banana | cherry"

// 자르기 (Cut — Go 1.18+)
before, after, found := strings.Cut("user=Alice", "=")
fmt.Println(before, after, found) // user Alice true
}

strings.Builder — 효율적인 문자열 구성

package main

import (
"fmt"
"strings"
)

func buildSQL(table string, conditions []string) string {
var b strings.Builder

b.WriteString("SELECT * FROM ")
b.WriteString(table)

if len(conditions) > 0 {
b.WriteString(" WHERE ")
for i, cond := range conditions {
if i > 0 {
b.WriteString(" AND ")
}
b.WriteString(cond)
}
}
b.WriteByte(';')

return b.String()
}

func main() {
sql := buildSQL("users", []string{"age > 18", "active = true", "country = 'KR'"})
fmt.Println(sql)
// SELECT * FROM users WHERE age > 18 AND active = true AND country = 'KR';

// strings.Builder — + 연산보다 훨씬 빠름 (메모리 재할당 최소화)
var sb strings.Builder
for i := 0; i < 5; i++ {
fmt.Fprintf(&sb, "항목 %d\n", i+1) // fmt.Fprintf도 사용 가능
}
fmt.Print(sb.String())
}

strconv 패키지 — 타입 변환

package main

import (
"fmt"
"strconv"
)

func main() {
// 정수 ↔ 문자열
s := strconv.Itoa(42) // int → string: "42"
n, err := strconv.Atoi("123") // string → int
if err != nil {
fmt.Println("변환 실패:", err)
}
fmt.Println(s, n) // "42" 123

// 다양한 진수로 정수 포맷
fmt.Println(strconv.FormatInt(255, 2)) // "11111111" — 2진수
fmt.Println(strconv.FormatInt(255, 8)) // "377" — 8진수
fmt.Println(strconv.FormatInt(255, 16)) // "ff" — 16진수

// 문자열 → 정수 (진수 지정)
v, _ := strconv.ParseInt("ff", 16, 64) // 16진수 파싱
fmt.Println(v) // 255
v2, _ := strconv.ParseInt("11111111", 2, 64) // 2진수 파싱
fmt.Println(v2) // 255

// 부동소수점 변환
fs := strconv.FormatFloat(3.14159, 'f', 2, 64) // "3.14" — 소수점 2자리
fmt.Println(fs)
fv, _ := strconv.ParseFloat("3.14", 64)
fmt.Println(fv) // 3.14

// 불리언 변환
bs := strconv.FormatBool(true) // "true"
bv, _ := strconv.ParseBool("true") // true
fmt.Println(bs, bv)

// 문자열 이스케이프
quoted := strconv.Quote(`He said "Hello"`)
fmt.Println(quoted) // "He said \"Hello\""
unquoted, _ := strconv.Unquote(`"He said \"Hello\""`)
fmt.Println(unquoted) // He said "Hello"

// 에러 타입 확인
_, err = strconv.Atoi("abc")
if numErr, ok := err.(*strconv.NumError); ok {
fmt.Println("함수:", numErr.Func) // Atoi
fmt.Println("입력:", numErr.Num) // abc
fmt.Println("에러:", numErr.Err) // invalid syntax
}
}

unicode 패키지 — 문자 분류와 변환

package main

import (
"fmt"
"unicode"
"unicode/utf8"
)

func main() {
chars := []rune{'A', 'z', '5', ' ', '\t', '한', '!'}

for _, r := range chars {
fmt.Printf("'%c' — 문자:%v 숫자:%v 공백:%v 대문자:%v 소문자:%v\n",
r,
unicode.IsLetter(r),
unicode.IsDigit(r),
unicode.IsSpace(r),
unicode.IsUpper(r),
unicode.IsLower(r),
)
}

// 대소문자 변환
fmt.Println(string(unicode.ToUpper('a'))) // A
fmt.Println(string(unicode.ToLower('A'))) // a

// unicode/utf8 — UTF-8 바이트 처리
s := "Hello, 한국어!"

// 문자열의 룬(문자) 개수 — len()은 바이트 수를 반환
fmt.Println("바이트 수:", len(s)) // 17 (한글 3바이트)
fmt.Println("룬(문자) 수:", utf8.RuneCountInString(s)) // 11

// 유효한 UTF-8인지 확인
fmt.Println("유효한 UTF-8:", utf8.ValidString(s)) // true

// 첫 번째 룬과 그 크기
r, size := utf8.DecodeRuneInString(s)
fmt.Printf("첫 룬: %c, 크기: %d 바이트\n", r, size) // H, 1

// 룬 단위로 순회 (range는 자동으로 UTF-8 디코딩)
for i, r := range s {
if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
fmt.Printf("위치 %d: 비문자 '%c'\n", i, r)
}
}
}

regexp 패키지 — 정규 표현식

기본 사용법

package main

import (
"fmt"
"regexp"
)

func main() {
// Compile — 에러를 반환 (사용자 입력 처리 시 사용)
re, err := regexp.Compile(`\d+`)
if err != nil {
fmt.Println("정규식 오류:", err)
return
}

// MustCompile — 패닉 발생 (패키지 레벨 변수에 적합)
digitRe := regexp.MustCompile(`\d+`)

s := "나이: 25, 점수: 98, 순위: 3"

// 일치 여부 확인
fmt.Println(re.MatchString("abc123")) // true
fmt.Println(digitRe.MatchString("abc")) // false

// 첫 번째 일치 문자열 찾기
fmt.Println(digitRe.FindString(s)) // "25"
fmt.Println(digitRe.FindStringIndex(s)) // [4 6] — 바이트 인덱스

// 모든 일치 문자열 찾기
fmt.Println(digitRe.FindAllString(s, -1)) // [25 98 3]
fmt.Println(digitRe.FindAllString(s, 2)) // [25 98] — 최대 2개

// 교체
result := digitRe.ReplaceAllString(s, "##")
fmt.Println(result) // "나이: ##, 점수: ##, 순위: ##"

// 함수로 교체
result2 := digitRe.ReplaceAllStringFunc(s, func(match string) string {
return "[" + match + "]"
})
fmt.Println(result2) // "나이: [25], 점수: [98], 순위: [3]"

// 분할
spaceRe := regexp.MustCompile(`\s+`)
fmt.Println(spaceRe.Split("hello world\tfoo", -1)) // [hello world foo]
}

캡처 그룹

package main

import (
"fmt"
"regexp"
)

func main() {
// 캡처 그룹 — 괄호로 묶인 부분
dateRe := regexp.MustCompile(`(\d{4})-(\d{2})-(\d{2})`)

date := "오늘은 2024-01-15 입니다"

// FindStringSubmatch — 전체 일치 + 그룹
match := dateRe.FindStringSubmatch(date)
if match != nil {
fmt.Println("전체:", match[0]) // "2024-01-15"
fmt.Println("연도:", match[1]) // "2024"
fmt.Println("월:", match[2]) // "01"
fmt.Println("일:", match[3]) // "15"
}

// FindAllStringSubmatch — 모든 일치 + 그룹
text := "시작: 2024-01-15, 종료: 2024-12-31"
allMatches := dateRe.FindAllStringSubmatch(text, -1)
for _, m := range allMatches {
fmt.Printf("날짜: %s (연: %s, 월: %s, 일: %s)\n",
m[0], m[1], m[2], m[3])
}

// 이름 있는 캡처 그룹
namedRe := regexp.MustCompile(`(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})`)
m := namedRe.FindStringSubmatch("2024-01-15")
if m != nil {
names := namedRe.SubexpNames()
for i, name := range names {
if name != "" {
fmt.Printf("%s: %s\n", name, m[i])
}
}
}
}

실전 예제 1 — 이메일 유효성 검사기

package main

import (
"fmt"
"regexp"
"strings"
)

// 패키지 레벨에서 컴파일 — 한 번만 컴파일, 계속 재사용
var (
emailRe = regexp.MustCompile(
`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`,
)
phoneRe = regexp.MustCompile(`^(\+82|0)\d{9,10}$`)
)

type ValidationResult struct {
Valid bool
Errors []string
}

func validateEmail(email string) ValidationResult {
var errs []string

email = strings.TrimSpace(email)
if email == "" {
errs = append(errs, "이메일이 비어 있습니다")
return ValidationResult{false, errs}
}

if len(email) > 254 {
errs = append(errs, "이메일이 너무 깁니다 (최대 254자)")
}

if !emailRe.MatchString(email) {
errs = append(errs, "이메일 형식이 올바르지 않습니다")
}

parts := strings.Split(email, "@")
if len(parts) == 2 {
local := parts[0]
if strings.HasPrefix(local, ".") || strings.HasSuffix(local, ".") {
errs = append(errs, "로컬 부분이 점으로 시작하거나 끝날 수 없습니다")
}
if strings.Contains(local, "..") {
errs = append(errs, "로컬 부분에 연속된 점이 있습니다")
}
}

return ValidationResult{len(errs) == 0, errs}
}

func main() {
testEmails := []string{
"user@example.com",
"invalid.email",
"user@.com",
"user..name@example.com",
"valid+tag@domain.co.kr",
"",
"@no-local.com",
}

for _, email := range testEmails {
result := validateEmail(email)
if result.Valid {
fmt.Printf("✓ '%s' — 유효\n", email)
} else {
fmt.Printf("✗ '%s' — 오류: %s\n", email, strings.Join(result.Errors, ", "))
}
}
}

실전 예제 2 — 텍스트 파서 (로그 분석기)

package main

import (
"fmt"
"regexp"
"strconv"
"strings"
)

// 로그 패턴: [2024-01-15 10:30:00] [INFO] request_id=abc123 status=200 duration=150ms
var logRe = regexp.MustCompile(
`\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] \[(\w+)\] (.+)`,
)
var kvRe = regexp.MustCompile(`(\w+)=(\S+)`)

type LogEntry struct {
Timestamp string
Level string
Fields map[string]string
}

func parseLog(line string) (*LogEntry, bool) {
m := logRe.FindStringSubmatch(line)
if m == nil {
return nil, false
}

entry := &LogEntry{
Timestamp: m[1],
Level: m[2],
Fields: make(map[string]string),
}

// 키-값 쌍 파싱
kvMatches := kvRe.FindAllStringSubmatch(m[3], -1)
for _, kv := range kvMatches {
entry.Fields[kv[1]] = kv[2]
}

return entry, true
}

type LogStats struct {
TotalRequests int
ErrorCount int
TotalDuration int
StatusCodes map[string]int
}

func analyzeLog(lines []string) LogStats {
stats := LogStats{StatusCodes: make(map[string]int)}

for _, line := range lines {
entry, ok := parseLog(line)
if !ok {
continue
}

if entry.Level == "ERROR" {
stats.ErrorCount++
}

if status, ok := entry.Fields["status"]; ok {
stats.TotalRequests++
stats.StatusCodes[status]++
}

if dur, ok := entry.Fields["duration"]; ok {
dur = strings.TrimSuffix(dur, "ms")
if ms, err := strconv.Atoi(dur); err == nil {
stats.TotalDuration += ms
}
}
}

return stats
}

func main() {
logs := []string{
`[2024-01-15 10:30:00] [INFO] request_id=req-001 status=200 duration=150ms`,
`[2024-01-15 10:30:01] [INFO] request_id=req-002 status=200 duration=89ms`,
`[2024-01-15 10:30:02] [ERROR] request_id=req-003 status=500 duration=3200ms`,
`[2024-01-15 10:30:03] [WARN] request_id=req-004 status=429 duration=12ms`,
`[2024-01-15 10:30:04] [INFO] request_id=req-005 status=201 duration=234ms`,
`[2024-01-15 10:30:05] [ERROR] request_id=req-006 status=503 duration=5000ms`,
`서버 재시작 중...`, // 파싱 실패 예시
}

stats := analyzeLog(logs)

fmt.Printf("=== 로그 분석 결과 ===\n")
fmt.Printf("전체 요청: %d\n", stats.TotalRequests)
fmt.Printf("에러 수: %d\n", stats.ErrorCount)
if stats.TotalRequests > 0 {
avg := stats.TotalDuration / stats.TotalRequests
fmt.Printf("평균 응답 시간: %dms\n", avg)
}
fmt.Printf("상태 코드 분포:\n")
for code, count := range stats.StatusCodes {
fmt.Printf(" %s: %d건\n", code, count)
}
}

패키지 비교 요약

패키지주요 용도핵심 함수
strings문자열 조작Contains, Split, Join, Replace, Builder
strconv타입 변환Atoi, Itoa, ParseFloat, FormatInt
unicode문자 분류IsLetter, IsDigit, ToUpper
unicode/utf8UTF-8 처리RuneCountInString, DecodeRuneInString
regexp정규 표현식MustCompile, FindAllString, ReplaceAllString

정규 표현식은 강력하지만 단순한 문자열 검색에는 strings 패키지가 훨씬 빠릅니다. 고정된 패턴 검색에는 strings.Containsstrings.Index를, 복잡한 패턴에만 regexp를 사용하세요.