독특한 Go lang 의 String

roachroach
3 min read

len("Hello, 월드") 의 출력값이 몇이 나올거라고 생각하는가? 파이썬과 같은 언어에서는 10이 나오기를 기대한다. 하지만 Go lang 에서는 14가 나온다. 그 이유는 무엇일까?

package main

import (
    "fmt"
)

func main() {
    hello := "Hello, 월드!"

    fmt.Println("문자열 길이: ", len(hello))

    for i := 0; i < len(hello); i++ {
        fmt.Printf("타입: %T 값:%d 문자값: %c\n", hello[i], hello[i], hello[i])
    }
}

Go lang 의 len() 함수의 결과값이 14가 나오는 이유는 Go lang 에서는 문자열을 기본적으로 UTF-8 로 인코딩된 바이트 시퀀스로 다루기 때문이다. 즉, 14 바이트는 영어와 기호는 각 1byte, 한글이 각 3 byte 를 차지하기 때문에 (1*8 + 3 * 2) = 14 byte 가 나오게 된다. 여기서 "왜 한글은 3 byte 일까?" 라는 의문이 들 수 있다. 오늘은 그 의문을 한번 풀어보려고 한다.

UTF-8 이란?

UTF-8 은 문자를 인코딩하는 체계화된 방식중 하나이다. 기본적으로 code point 를 1~4 byte 사이로 인코딩하도록 되어 있다.

WikiPedia 의 정의된 방식을 따라보면 code point 값으로 u~z 까지의 값을 치환해주면 된다고 한다. 즉, 한글은 3 Byte 가 소모되므로 “월” 이라는 문자의 유니코드를 wwww xxxxyy yyzzzz 값으로 치환해주면 될 것이다.

그렇다면 월이라는 단어의 유니코드를 확인해보자. 검색 해보니 “월” 의 유니코드는 “U+C6D4” 이다. 이걸 한번 16진수로 변환해보자. Go lang 에서는 16 진수를 나타내기 위해서는 U+ 대신 0x 를 이용해야 한다.

    codePoint := rune(0xC6D4)
    binary16BitStr := fmt.Sprintf("%016b", codePoint)

    fmt.Println("16비트 이진수 표현:", binary16BitStr)

위와 같이 코드를 작성하고 실행시키면 1100 0110 1101 0100 이 나온다. 보기 좋게 하기 위해 4글자 마다 개행을 하나씩 넣었다. 우리가 아까 위에서 나온 3바이트 기준으로 w~z 까지의 값을 대치하기 위해서는 4+6+6 으로 대입되어야 한다.

즉, 11101100 10011011 10010100 이 나오게 될 것이다. “월” 이라는 단어는 3byte 로 표현가능하며 UTF-8에서는 이렇게 표기가 된다는 것을 계산해 볼수 있다. 그렇다면 한번 검증해보자. Go lang String 공식문서에 따르면 String 은 UTF-8 로 인코딩된 8bit 의 집합이라 적혀있으므로 %08b 를 통해서 출력 가능할 것이다.

    r := "월"
    c := string(r)
    for i := 0; i < len(c); i++ {
        fmt.Printf("%08b ", c[i]) // 11101100 10011011 10010100
    }

출력값을 보면 11101100 10011011 10010100 으로 우리가 계산한 값과 같은 것을 확인할 수 있다. 더 정확하게 하고 싶다면 비트 연산(bit operators) 를 이용해서도 가능하다. 첫번째 4비트는 16비트에서 12개를 right shift 하고 1111 로 비트 mask 를 하는 것으로 추출 가능하다.

    standard3Bytes := "1110wwww 10xxxxyy 10yyzzzz"
    fmt.Println("표준 3바이트 인코딩:", standard3Bytes) // "1110wwww 10xxxxyy 10yyzzzz"

    wwww := (r >> 12) & 0b1111
    xxxxyy := (r >> 6) & 0b111111
    yyzzzz := r & 0b111111
    fmt.Println("첫 4비트:", fmt.Sprintf("%04b", wwww))     // 1100
    fmt.Println("다음 6비트:", fmt.Sprintf("%06b", xxxxyy))  // 011011
    fmt.Println("마지막 6비트:", fmt.Sprintf("%06b", yyzzzz)) // 010100

    standard3Bytes = strings.Replace(standard3Bytes, "wwww", fmt.Sprintf("%04b", wwww), 1)
    standard3Bytes = strings.Replace(standard3Bytes, "xxxxyy", fmt.Sprintf("%06b", xxxxyy), 1)
    standard3Bytes = strings.Replace(standard3Bytes, "yyzzzz", fmt.Sprintf("%06b", yyzzzz), 1)
    fmt.Println("최종 3바이트 인코딩:", standard3Bytes) // 11101100 10011011 10010100
}

결과가 동일하게 나오는 것을 확인할 수 있다.

마치며

Go 에서는 이러한 부분을 rune 을 쓰면 해결 가능하다. 다만 이 UTF-8 에 대한 이해가 선행되어야 rune 과 같은 타입에 대한 이해가 이루어진다고 보기에 먼져 정리해보았다.

0
Subscribe to my newsletter

Read articles from roach directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

roach
roach

https://www.linkedin.com/feed/update/urn:li:activity:7092144087058825216/