본문 바로가기
프로그래밍/Golang

Golang string 과 rune / 한글(멀티바이트) 이슈

by slowin 2024. 8. 6.

개요

Go 언어에서 문자열(string)과 룬(rune)은 텍스트를 다룰때 주의할점과 특징들에 대해서 알아보겠습니다.

string(문자열)

Go에서 string은 불변(immutable)의 바이트 슬라이스입니다. 기본적으로 UTF-8로 인코딩된 텍스트를 나타내는 데 사용됩니다.

특징

  • 읽기 전용: 한 번 생성된 문자열은 변경할 수 없습니다.
  • UTF-8 인코딩: 기본적으로 UTF-8을 사용합니다.
  • 인덱싱: 개별 바이트에 접근할 수 있지만, 항상 유효한 유니코드 문자를 반환하지는 않습니다.

예제

s := "Hello, 월드"
fmt.Println(len(s)) // 13 (바이트 수)
fmt.Println(s[0])   // 72 (ASCII 값 'H')

rune(룬)

rune은 Go에서 유니코드 코드 포인트를 나타내는 타입입니다. int32의 별칭으로, 하나의 유니코드 문자를 표현합니다.

특징

  • 32비트 정수: 모든 유니코드 문자를 표현할 수 있습니다.
  • 문자 단위 처리: 멀티바이트 문자를 단일 단위로 처리합니다.
  • 문자열 순회에 유용: for range 루프와 함께 사용하면 문자열의 각 문자를 올바르게 처리할 수 있습니다.

예제

s := "Hello, 월드"
for _, r := range s {
    fmt.Printf("%c ", r)
}
// 출력: H e l l o ,   월 드

string과 rune의 관계

string은 바이트의 시퀀스지만, 대부분의 경우 우리는 문자열을 문자(character)의 시퀀스로 생각합니다. Go에서 이러한 문자는 rune으로 표현됩니다.

문자열을 rune 슬라이스로 변환

s := "Hello, 월드"
r := []rune(s)
fmt.Println(len(r)) // 8 (문자 수)

rune 슬라이스를 문자열로 변환

r := []rune{'H', 'e', 'l', 'l', 'o', '월', '드'}
s := string(r)
fmt.Println(s) // "Hello월드"

한글과 같은 멀티바이트 문자의 이슈

한글은 UTF-8 인코딩에서 3바이트를 차지합니다. 이로 인해 문자열 처리 시 몇 가지 주의할점을 알아두어야합니다!

이슈

  1. 문자열 길이: len() 함수는 바이트 수를 반환하므로, 한글을 포함한 문자열의 실제 문자 수와 일치하지 않습니다.
  2. 인덱싱: 문자열을 바이트 단위로 인덱싱하면 한글 문자의 중간 바이트에 접근할 수 있어 의미 없는 값이 나올 수 있습니다.
  3. 슬라이싱: 바이트 단위로 슬라이싱하면 한글 문자가 잘릴 수 있습니다.
s := "Hello, 세계"
fmt.Println(len(s))    // 13 (바이트 수)
fmt.Println(s[7])      // 236 (의미 없는 바이트 값)
fmt.Println(s[7:9])    // "세" (한글 문자의 일부만 포함)

byte배열 인덱스 접근시 한글 깨짐
한글 3바이트 예시

이처럼 한글은 3바이트 이기때문에 13바이트의 길이로 계산되어지는 것을 알 수 있어요.

rune과 utf8 패키지로 이슈 해결

Go 언어에서 이러한 이슈를 해결하기 위해 두 가지 접근방식에 대해서 알아볼께요.

  • rune 타입 사용:
    • rune은 하나의 Code points로 나타내므로, 멀티바이트 문자를 정상적으로 처리할수 있습니다.
    • for range 루프는 문자열을 자동으로 rune 단위로 순회합니다.
  • utf8 패키지 활용:
    • utf8 패키지는 UTF-8 인코딩된 텍스트를 다루기 위한 함수들을 제공합니다.
      • Valid(b []byte) bool
        • 주어진 바이트 슬라이스가 유효한 UTF-8 인코딩인지 검사합니다.
      • ValidRune(r rune) bool
        • 주어진 룬이 유효한 유니코드 코드 포인트인지 검사합니다.
      • RuneStart(b byte) bool
        • 주어진 바이트가 UTF-8 인코딩의 첫 바이트인지 검사합니다.
      • EncodeRune(p []byte, r rune) int
        • 주어진 룬을 UTF-8로 인코딩하여 바이트 슬라이스에 저장합니다.
      • DecodeLastRune(p []byte) (r rune, size int)
        • UTF-8로 인코딩된 바이트 슬라이스에서 마지막 룬을 디코딩합니다.
      • RuneLen(r rune) int
        • 주어진 룬을 UTF-8로 인코딩했을 때 필요한 바이트 수를 반환합니다.

문자 수 세기

s := "Hello, 월드"
fmt.Println(utf8.RuneCountInString(s)) // 9

  • utf8.RuneCountInString() 함수는 문자열에 포함된 유니코드 문자(rune)의 수를 세는 함수입니다.
  • 이 함수는 멀티바이트 문자(예: 한글)도 하나의 문자로 취급합니다.
  • 결과값 9는 'H', 'e', 'l', 'l', 'o', ',', ' ', '월', '드' 총 9개의 문자를 의미합니다.
  • 단순히 len(s)를 사용했다면 바이트 수인 13가 반환됩니다.

안전한 인덱싱과 슬라이싱

s := "Hello, 월드"
runes := []rune(s)
fmt.Println(string(runes[7]))   // "월" (올바른 문자 출력)
fmt.Println(string(runes[7:]))  // "월드" (올바른 슬라이싱)

  • 문자열 s를 []rune 슬라이스로 변환합니다. 이 과정에서 각 문자는 하나의 rune으로 변환됩니다.
  • runes[7]은 8번째 문자(0부터 시작)인 '월'에 해당합니다.
  • string(runes[7])은 해당 rune을 다시 문자열로 변환합니다.
  • runes[7:]는 8번째 문자부터 끝까지의 슬라이스를 의미하며, 이를 문자열로 변환하면 "월드"가 됩니다.
  • 이 방법은 바이트 단위로 인덱싱하거나 슬라이싱할 때 발생할 수 있는 문제(멀티바이트 문자 깨짐)를 방지합니다.

문자열 순회

s := "Hello, 월드"
for i, r := range s {
    fmt.Printf("%d: %c\n", i, r)
}
// 출력:
// 0: H
// 1: e
// 2: l
// 3: l
// 4: o
// 5: ,
// 6:  
// 7: 월
// 10: 드

  • for range 루프는 문자열을 자동으로 rune 단위로 순회합니다.
  • i는 각 rune의 시작 바이트 인덱스를 나타냅니다.
  • r은 각 rune(유니코드 코드 포인트)을 나타냅니다.
  • 출력 결과를 보면:
    • 영문자와 특수문자는 각각 1바이트를 차지하므로 인덱스가 1씩 증가합니다.
    • '월'은 3바이트를 차지하므로 그 다음 문자 '드'의 인덱스는 7에서 10으로 건너뜁니다.
  • range 순회방법은 멀티바이트 문자를 포함한 문자열을 안전하게 순회할 수 있게 해줍니다.

정리

  • Go 언어에서 문자열 처리 시 rune 타입과 utf8 패키지 활용이 중요
  • 멀티바이트 문자 처리 시 항상 주의 필요
  • 안전한 문자열 처리를 위해 제공되는 기능들을 적절히 활용해야 함

참고

https://go.dev/blog/strings