유니코드와 UTF-8 함수 사용하기

이재홍 http://www.pyrasis.com 2014.12.17 ~ 2015.02.07

UTF-8 함수 사용하기

UTF-8은 유니코드를 저장하거나 전송할 때 사용하는 인코딩 방식 중의 하나입니다(UTF-8 이외에도 UTF-7, UTF-16, UTF-32 등 다양한 방식이 있습니다). Go 언어에서는 UTF-8을 주로 사용하므로 UTF-8에 대해 자세히 알아보겠습니다.

다음은 unicode/utf8 패키지에서 제공하는 함수입니다.

  • func RuneLen(r rune) int: 문자의 바이트 수를 구함
  • func RuneCountInString(s string) (n int): 문자열의 실제 길이를 구함
  • func DecodeRune(p []byte) (r rune, size int): byte 슬라이스에서 첫 글자를 디코딩함
  • func DecodeLastRune(p []byte) (r rune, size int): byte 슬라이스에서 마지막 글자를 디코딩함
  • func DecodeRuneInString(s string) (r rune, size int): 문자열에서 첫 글자를 디코딩함
  • func DecodeLastRuneInString(s string) (r rune, size int): 문자열에서 마지막 글자를 디코딩함
  • func Valid(p []byte) bool: byte 슬라이스가 UTF-8이 맞는지 확인
  • func ValidRune(r rune) bool: rune 변수에 저장된 값이 UTF-8이 맞는지 확인
  • func ValidString(s string) bool: 문자열이 UTF-8이 맞는지 확인

영문자를 저장할 때는 문자 하나가 1바이트를 차지합니다. 하지만 한글, 한자, 일본어 같은 문자는 1바이트로 저장할 수가 없기 때문에 보통 2바이트로 저장합니다(Multi Byte Character Set, MBCS). UTF-8은 가변 길이 문자 인코딩 방식이라 문자를 저장할 때 1바이트에서 4바이트까지 사용하며 한글은 3바이트로 저장합니다.

먼저 한글 글자 하나의 길이(바이트 수)를 구해보겠습니다.

package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	var s string = "한"
	fmt.Println(len(s)) // 3: 한글은 3바이트로 저장하므로 3

	var r rune = '한'
	fmt.Println(utf8.RuneLen(r)) // 3: 한글은 3바이트로 저장하므로 3
}

실행 결과

3
3

len 함수를 사용하면 string 변수에 저장된 한글의 바이트 수를 구할 수 있습니다. 그리고 utf8.RuneLen 함수를 사용하면 rune 변수에 저장된 한글 글자 하나의 바이트 수를 구할 수 있습니다. 여기서 한글 글자 하나는 3바이트를 차지하므로 3이 출력됩니다.

이제 한글 문자열에서 바이트 수가 아닌 실제 길이(글자 개수)를 구해보겠습니다.

package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	var s1 string = "안녕하세요"
	fmt.Println(utf8.RuneCountInString(s1)) // 5: "안녕하세요"의 실제 길이는 5
}

실행 결과

5

utf8.RuneCountInString 함수를 사용하면 문자열의 실제 글자 개수를 구할 수 있습니다.

이번에는 한글 문자열에서 글자를 디코딩해보겠습니다.

package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	b := []byte("안녕하세요")

	r, size := utf8.DecodeRune(b)
	fmt.Printf("%c %d\n", r, size) // 안 3: "안녕하세요"의 첫 글자를 디코딩하여 '안', 바이트 수 3

	r, size = utf8.DecodeRune(b[3:]) // '안'의 길이가 3이므로 인덱스 3부터 부분 슬라이스를 만들면 "녕하세요"가 됨
	fmt.Printf("%c %d\n", r, size)   // 녕 3: "녕하세요"를 첫 글자를 디코딩하여 '녕', 바이트 수 3

	r, size = utf8.DecodeLastRune(b)
	fmt.Printf("%c %d\n", r, size) // 요 3: "안녕하세요"의 마지막 글자를 디코딩하여 '요', 바이트 수 3

	// '요'의 길이가 3이므로 // 문자열 길이-3을 하여 부분 슬라이스를 만들면
	// "안녕하세"가 됨
	r, size = utf8.DecodeLastRune(b[:len(b)-3])

	fmt.Printf("%c %d\n", r, size) // 세 3: "안녕하세"의 마지막 글자를 디코딩하여 '세', 바이트 수 3
}

실행 결과

안 3
녕 3
요 3
세 3
  • utf8.DecodeRune 함수는 byte 슬라이스에서 첫 글자를 디코딩하여 리턴하고, 디코딩된 바이트 수도 리턴합니다. 글자 하나만 디코딩하며 다음 글자를 디코딩하려면 b[3:]과 같이 부분 슬라이스로 만들어주면 됩니다.
  • utf8.DecodeLastRune 함수는 byte 슬라이스에서 마지막 글자를 디코딩하여 리턴하고, 디코딩된 바이트 수도 리턴합니다. 글자 하나만 디코딩하며 다른 글자를 디코딩하려면 b[:len(b)-3]과 같이 부분 슬라이스로 만들어주면 됩니다.

그러면 문자열의 첫 글자와 마지막 글자를 구하려면 어떻게 해야 할까요? 다음은 영문 문자열의 첫 글자와 마지막 글자를 구합니다.

package main

import "fmt"

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

	fmt.Printf("%c\n", s[0])        // H: 인덱스 0이 첫 번째 글자
	fmt.Printf("%c\n", s[len(s)-1]) // !: 문자열 길이에서 1을 뺀 인덱스가 마지막 글자
}

실행 결과

H
!

영문 문자열은 인덱스 0이 첫 번째 글자이고 문자열 길이에서 1을 뺀 인덱스(인덱스는 0부터 시작하므로)가 마지막 글자입니다.

한글 문자열은 UTF-8에서 3바이트로 저장되므로 인덱스로 접근하면 한글이 정상적으로 출력되지 않습니다.

package main

import "fmt"

func main() {
	s := "안녕하세요"

	fmt.Printf("%c\n", s[0])        // 3바이트 중 1바이트만 출력하므로 한글이 정상적으로 출력되지 않음
	fmt.Printf("%c\n", s[len(s)-1]) // 3바이트 중 1바이트만 출력하므로 한글이 정상적으로 출력되지 않음
}

따라서 한글 문자열의 첫 글자와 마지막 글자를 구하려면 다음과 같이 utf8.DecodeRuneInString, utf8.DecodeLastRuneInString 함수를 사용하면 됩니다.

package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	s := "안녕하세요"

	r, _ := utf8.DecodeRuneInString(s) // UTF-8 문자열의 첫 글자와 바이트 수를 리턴
	fmt.Printf("%c\n", r)              // 안: 문자열의 첫 글자

	r, _ = utf8.DecodeLastRuneInString(s) // UTF-8 문자열의 첫 글자와 바이트 수를 리턴
	fmt.Printf("%c\n", r)                 // 요: 문자열의 마지막 글자
}

실행 결과

안
요
  • utf8.DecodeRuneInString 함수는 UTF-8 문자열에서 첫 글자를 디코딩하여 리턴하고, 디코딩된 바이트 수도 리턴합니다.
  • utf8.DecodeLastRuneInString 함수는 UTF-8 문자열에서 마지막 글자를 디코딩하여 리턴하고, 디코딩된 바이트 수도 리턴합니다.

값이나 문자열이 UTF-8이 맞는지 확인하는 방법은 다음과 같습니다.

package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	var b1 []byte = []byte("안녕하세요")
	fmt.Println(utf8.Valid(b1)) // true: "안녕하세요"는 UTF-8이 맞으므로 true
	var b2 []byte = []byte{0xff, 0xf1, 0xc1}
	fmt.Println(utf8.Valid(b2)) // false: 0xff 0xf1 0xc1은 UTF-8이 아니므로 false

	var r1 rune = '한'
	fmt.Println(utf8.ValidRune(r1)) // true: '한'은 UTF-8이 맞으므로 true
	var r2 rune = 0x11111111
	fmt.Println(utf8.ValidRune(r2)) // false: 0x11111111은 UTF-8이 아니므로 false

	var s1 string = "한글"
	fmt.Println(utf8.ValidString(s1)) // true: "한글"은 UTF-8이 맞으므로 true
	var s2 string = string([]byte{0xff, 0xf1, 0xc1})
	fmt.Println(utf8.ValidString(s2)) // false: 0xff 0xf1 0xc1은 UTF-8이 아니므로 false
}
  • utf8.Valid 함수는 byte 슬라이스가 UTF-8이 맞는지 확인합니다. UTF-8이 맞으면 true, 아니면 false를 리턴합니다.
  • utf8.ValidRune 함수는 rune 변수에 저장된 값이 UTF-8이 맞는지 확인합니다. UTF-8이 맞으면 true, 아니면 false를 리턴합니다.
  • utf8.ValidString 함수는 문자열이 UTF-8이 맞는지 확인합니다. UTF-8이 맞으면 true, 아니면 false를 리턴합니다.

저작권 안내

이 웹사이트에 게시된 모든 글의 무단 복제 및 도용을 금지합니다.
  • 블로그, 게시판 등에 퍼가는 것을 금지합니다.
  • 비공개 포스트에 퍼가는 것을 금지합니다.
  • 글 내용, 그림을 발췌 및 요약하는 것을 금지합니다.
  • 링크 및 SNS 공유는 허용합니다.

Published

01 June 2015