Programming/Golang

[번역] Strings, bytes, runes and characters in Go

chronosa 2021. 7. 2. 20:26

Code wars 문제를 풀던 중 Golang의 문자열이 좀 헷갈려서 금번에 공부할 목적으로 The Go Blog의 Strings, bytes, runes and characters in Go를 번역하였습니다. 원문은 위 링크를 참조하시기 바라며, 영어를 잘하는 편이 아니기도 하고, 구글 번역기를 기반으로 번역한 내용이기 때문에 내용에 의문점이 드신다면 바로 위 포스트에서 원문을 읽으시기를 바랍니다.

 

목차

    Strings, bytes, runes and characters in Go

    Rob Pike
    23 October 2013

    Introduction

    이전 블로그 게시물에서는 구현 뒤에 숨겨져 있는 동작원리를 설명하기 위해 여러 예제를 통해 slices가 Go에서 작동하는 방식을 설명했습니다. 이러한 배경을 바탕으로 이 게시물에서는 Go의 문자열에 대해 설명합니다. 문자열이 블로그 게시물에 개시하기에는 너무 단순한 주제로 보일 수 있지만, 이를 잘 사용하려면 작동 방식을 이해하는 것 뿐 아니라 byte, a character 그리고 룬 간의 차이 그리고 Unicode와 UTF-8의 차이, 그리고 문자열과 문자열 리터럴의 차이, 그리고 그 외의 다른 미묘한 차이를 이해해야 합니다.

    이 주제에 접근하는 한 가지 방법은 "n 번째 위치에서 Go 문자열을 인덱싱 할 때 n 번째 문자를 얻지 못하는 이유는 무엇입니까?"라는 자주 묻는 질문에 대한 대답으로 생각하는 것입니다. 보시다시피, 이 질문은 현대 사회에서 텍스트가 어떻게 작동하는지에 대한 많은 세부 사항으로 이어집니다.

    Go와는 별개로 이러한 문제 중 일부에 대한 훌륭한 소개는 Joel Spolsky의 유명한 블로그 게시물인 The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)입니다. 그가 제기 한 많은 포인트가 여기에 반영 될 것입니다.

    What is a string?

    몇 가지 기본 사항부터 시작하겠습니다.

    Go에서 문자열은 사실상 읽기 전용 바이트 조각(read-only slice of bytes)입니다. slice of bytes가 무엇인지 또는 어떻게 작동하는지 확실하지 않은 경우 이전 블로그 게시물을 읽어보십시오. 여기서 우리는 당신이 이것에 대해 이해하고 있다고 가정합니다.

    문자열이 임의의 바이트를 가지고 있다는 것은 매우 중요합니다. Unicode text, UTF-8 text 또는 기타 사전정의된 형식을 보유할 필요가 없습니다. 즉, 문자열의 내용은 바이트 조각(=slice of bytes)와 정확히 동일합니다.

    다음은 \ xNN 표기법을 사용하여 고유 한 바이트 값을 보유하는 문자열 상수를 정의하는 문자열 리터럴입니다. (물론 바이트 범위는 16 진수 값 00에서 FF까지입니다.)

    const sample = "\xbd\xb2\x3d\xbc\x20\xe2\x8c\x98"

     

    *참조 - byte

    바이트의 실질적 의미는 ASCII 문자 하나를 나타낼 수 있다는 것이다. 따라서 여러 바이트를 한 워드로 사용하고 있는 현재에도 대부분의 컴퓨터 하드웨어에서 메모리의 주소 단위로 사용된다. - 위키피디아

    Printing strings

    샘플 문자열의 일부 bytes는 유효한 ASCII나 UTF-8도 아니기 때문에 문자열을 직접 출력하면 보기에 안좋은 결과물이 생성됩니다.
    간단한 print 문은 다음과 같은 혼란을 야기합니다 (정확한 모양은 환경에 따라 다름).

    fmt.Println(sample)
    >> ��=� ⌘

    이 문자열(string)이 실제로 무엇을 담고 있는지 알아 내기 위해 우리는 이것의 개별적인 부분들을 조사해야합니다. 여기에는 다양한 방법이 있는데, 가장 확실한 방법은 아래의 for 루프와 같이 내용을 반복하고 bytes를 개별적으로 가져 오는 것입니다.

    for i := 0; i < len(sample); i++ {
            fmt.Printf("%x ", sample[i])
    }
    

    앞에서 보았듯이, 문자열 인덱싱은 문자(character)가 아닌 개별 byte에 액세스합니다. 아래에서 그 주제에 대해 자세히 살펴보는 걸로 하고, 지금은 바이트에 초점을 맞춰봅시다. 다음은 byte 별 loop의 출력입니다.

    >> bd b2 3d bc 20 e2 8c 98

    개별 byte가 문자열에서 정의한 16 진수 escapes와 어떻게 일치하는지 확인하십시오. (→ 위의 \xNN 표기법)

    위의 for loop보다 더 짧고 지저분하게 출력하는 방법은 fmt.Printf의 %x (16 진수) 형식을 사용하는 것입니다. 이렇게 하면 문자열의 순차 바이트(=sequential bytes)를 byte 당 2 개의 16 진수로 dump합니다.

    fmt.Printf("%x\n", sample)
    >> bdb23dbc20e28c98

    더 좋은 방법은 동일한 형식에 "space" flag를 사용하여 %와 x 사이에 공백을 두는 것입니다.

    fmt.Printf("% x\n", sample)
    >> bd b2 3d bc 20 e2 8c 98

    한 가지 더 있습니다. %q (따옴표로 묶인) 형식은 문자열에서 non-printable byte sequences를 escape하므로 출력이 모호하지 않습니다. 이 방법은 문자열의 많은 부분이 텍스트로 인식 되지만 제거해야 할 특이성이 있을 때 유용합니다. 다음을 생성합니다.

    fmt.Printf("%q\n", sample)
    >> "\xbd\xb2=\xbc ⌘"

    자세히 보면 노이즈에 묻혀있는 하나의 ASCII 등호(=)와 일반 공백(' ')이 있고 끝에는 잘 알려진 스웨덴어 "관심 장소"기호(⌘)가 나타납니다. 이 기호는 유니코드값 U+2318을 가지며 공백 (16진수 값 20) 뒤의 바이트 (e2 8c 98)에 의해 UTF-8로 인코딩됩니다.

    문자열의 이상한 값에 익숙하지 않거나 혼란스럽다면 %q 에 "플러스"플래그를 사용할 수 있습니다. 이 플래그는 non-printable sequences 뿐 아니라 Non-ASCII byte도 escape하도록하며 모두 UTF-8로 해석합니다. 그 결과 문자열에서 Non-ASCII 데이터를 나타내는 올바른 형식의 UTF-8 유니코드 값이 노출됩니다.

    fmt.Printf("%+q\n", sample)
    >> "\xbd\xb2=\xbc \u2318"

    이러한 printing 방법은 문자열의 내용을 디버깅 할 때 알아두면 좋으며 뒤에 나오는 논의에서 유용 할 것입니다.
    또한 이러한 모든 method는 byte slices에 대해 문자열과 동일하게 작동한다는 점에 주목할만한 가치가 있습니다.

    다음은 브라우저에서 바로 실행 (및 편집) 할 수있는 완전한 프로그램으로 나열된 printing options의 full set입니다.

    (* 원 게시글 참조)

    UTF-8 and string literals

    우리가 본 것처럼 string을 인덱싱하면 character가 아닌 byte가 생성됩니다. string은 단지 byte의 무리입니다. 즉, string에 character value 값을 저장할 때 한 번에 바이트 표현을 저장합니다. 이것이 어떻게 일어나는지 보기 위해 좀 더 통제된 예제를 살펴보겠습니다.

    func main() {
        const placeOfInterest = `⌘`
    
        fmt.Printf("plain string: ")
        fmt.Printf("%s", placeOfInterest)
        fmt.Printf("\n")
    
        fmt.Printf("quoted string: ")
        fmt.Printf("%+q", placeOfInterest)
        fmt.Printf("\n")
    
        fmt.Printf("hex bytes: ")
        for i := 0; i < len(placeOfInterest); i++ {
            fmt.Printf("%x ", placeOfInterest[i])
        }
        fmt.Printf("\n")
    }

    출력은 아래와 같습니다.

    plain string: ⌘
    quoted string: "\u2318"
    hex bytes: e2 8c 98

    유니코드 문자 값 U+2318, "Place of Interest"기호 ⌘는 바이트 e2 8c 98로 표시되고 해당 바이트는 16진수 값 2318의 UTF-8 인코딩임을 상기시킵니다.

    UTF-8에 대한 친숙도에 따라 명확하거나 미묘할 수 있지만, 문자열의 UTF-8 표현이 어떻게 생성되었는지 설명하는 데 시간을 할애 할 가치가 있습니다. 간단한 사실은 소스코드가 작성 될 때 만들어졌다는 것입니다.

     

    Go의 소스 코드는 UTF-8 텍스트로 정의됩니다. 다른 표현은 허용되지 않습니다. 이는 소스코드에서 아래의 텍스트를 작성할 때 프로그램을 만드는 데 사용 된 text editor는 ⌘ 기호의 UTF-8 인코딩을 소스 텍스트에 배치합니다. 우리가 16 진수 bytes를 print 할 때, 우리는 단지 editor가 파일에 배치한 데이터를 그대로 덤프합니다.

    `⌘`

    간단히 말해서, Go 소스코드는 UTF-8이므로 문자열 리터럴의 소스코드는 UTF-8 텍스트입니다. 만약 해당 문자열 리터럴에 raw string이 할 수 없는 (escape sequences가 없는 문자)를 포함하고 있는 경우, 생성 된 문자열이 따옴표 사이의 소스 텍스트를 정확하게 포함합니다. 따라서 정의(definition)과 구성에 따라 원시 문자열(raw string)은 항상 유효한 UTF-8 표현을 포함합니다. 마찬가지로, 이전 섹션과 같이 UTF-8-breaking escape가 포함되어 있지 않더라도 일반 문자열 리터럴에도 항상 유효한 UTF-8이 포함됩니다.

     

    어떤 사람들은 Go 문자열이 항상 UTF-8이라고 생각하지만 그렇지 않습니다. 문자열 리터럴만 UTF-8입니다. 이전 섹션에서 살펴본 것처럼 문자열 값은 임의의 bytes를 포함 할 수 있습니다. 여기에서 보여주었듯이 문자열 리터럴은 바이트 수준 이스케이프가없는 한 항상 UTF-8 텍스트를 포함합니다.

    요약하자면 문자열은 임의의 byte를 포함 할 수 있지만 문자열 리터럴에서 생성 될 때 해당 byte는 (거의 항상) UTF-8입니다.

    Code points, characters, and runes

    지금까지 byte"와 "character"라는 단어를 사용하는 방법에 대해 매우 주의했습니다. 문자열이 바이트를 보유하기 때문이기도 하고,  "character"라는 개념을 정의하기가 조금 어렵기 때문입니다. 유니코드 표준은 "코드 포인트"라는 용어를 사용하여 단일 값으로 표시되는 항목을 나타냅니다. 16 진수 값이 2318 인 Code point U+2318은 기호 ⌘를 나타냅니다. (해당 코드 포인트에 대한 자세한 정보는 유니코드 페이지를 참조하십시오.)

     

    좀 더 단순한 예를 선택해서 보면, 유니코드 Code point U+0061은 소문자 라틴 문자 'a'입니다.

    하지만 소문자 'à'는 어떻습니까? 그것은 문자이며 Code point (U+00E0)이기도하지만 다른 표현을 가지고 있습니다. 예를 들어 "결합" grave accent Code point (U+0300) ('`')에 소문자 a (U+0061)를 연결하여 동일한 문자 à를 만들 수 있습니다. 일반적으로 문자는 Code point의 여러 시퀀스로 표현 될 수 있으며, 이에 따라 UTF-8 바이트의 시퀀스가 달라집니다.

     

    따라서 컴퓨팅에서 문자의 개념은 모호하거나 혼란스럽기 때문에 신중하게 사용합니다. 이를 신뢰할 수 있게 만들기 위해 주어진 문자가 항상 동일한 Code point로 표현되도록 보장하는 normalization 기술이 있지만 그 주제는 현재 주제에서 너무 멀리 떨어져 있습니다. 이후 블로그 게시물에서는 Go 라이브러리가 normalization을 처리하는 방법을 설명합니다.

     

    "Code point"는 꽤나 복잡하기 때문에, Go는 이 개념에 대한 짧은 용어인 rune을 소개합니다. 이 용어는 libraries와 source code에 나타나며 "Code point"와 정확히 동일한 의미로 사용되며 흥미로운 추가 사항이 하나 있습니다.


    Go 언어는 rune을 int32 유형의 alias로 정의하므로, 정수 값이 코드 포인트를 나타낼 때 프로그램이 명확해질 수 있습니다. 또한, character constant(문자 상수)라고 생각할 수있는 것은 Go에서 rune constant라고합니다. 아래의 표현식의 유형 및 값은 정수 값 0x2318을 가진 rune입니다.

    '⌘'

    요약하면 다음과 같은 특징이 있습니다.

    • Go 소스 코드는 항상 UTF-8입니다.
    • 문자열은 임의의 bytes를 보유합니다.
    • byte-level escapes가 없는 문자열 리터럴은 항상 유효한 UTF-8 시퀀스를 보유합니다.
    • 이러한 시퀀스는 rune 이라고 불리는 유니코드 Code point를 나타냅니다.
    • Go에서는 문자열의 문자가 정규화된다는 보장이 없습니다.

    Range loops

    Go 소스 코드가 UTF-8이라는 자명한 세부 사항 외에도 Go가 UTF-8을 특별히 처리하는 유일한 방법은 문자열에서 for range loop를 사용할 때입니다.

     

    우리는 일반 for loop에서 어떤 일이 일어나는지 보았습니다. for range loop는 대조적으로, 반복 할 때마다 UTF-8로 인코딩 된 rune 하나를 디코딩합니다. loop를 돌 때마다 loop의 index는 현재 rune의 시작 위치이며, 이는 bytes 단위로 측정되고, Code point는 해당 값입니다. 다음은 Code point의 유니코드 값과 printed 된 표현을 보여주는 또 다른 편리한 Printf 형식 인 %#U를 사용하는 예제입니다.

    const nihongo = "日本語"
    for index, runeValue := range nihongo {
        fmt.Printf("%#U starts at byte position %d\n", runeValue, index)
    }

    출력은 각 Code point가 여러 bytes를 차지하는 방법을 보여줍니다.

    U+65E5 '日' starts at byte position 0
    U+672C '本' starts at byte position 3
    U+8A9E '語' starts at byte position 6

    Libraries

    Go의 표준 라이브러리는 UTF-8 텍스트 해석을 강력하게 지원합니다. for range loop가 목적에 충분하지 않은 경우 라이브러리의 패키지에서 필요한 기능을 제공 할 가능성이 있습니다.

     

    가장 중요한 패키지는 unicode/utf8로 UTF-8 문자열의 유효성을 검사, 분해 및 재조립하는 helper routines이 포함되어 있습니다. 다음은 위의 range 예제와 동일한 프로그램이지만 해당 패키지의 DecodeRuneInString 함수를 사용하여 작업을 수행합니다. 함수의 반환 값은 rune과 UTF-8로 인코딩 된 bytes의 너비입니다.

    const nihongo = "日本語"
    for i, w := 0, 0; i < len(nihongo); i += w {
        runeValue, width := utf8.DecodeRuneInString(nihongo[i:])
        fmt.Printf("%#U starts at byte position %d\n", runeValue, i)
        w = width
    }

    for range loop와 DecodeRuneInString은 정확히 동일한 반복 시퀀스를 생성하도록 정의됩니다.

    Conclusion

    처음에 제기된 질문에 답하면 : 문자열은 bytes로 구성되므로 인덱싱하면 characters가 아닌 bytes가 생성됩니다. 문자열은 characters를 포함하지 않을 수도 있습니다. 사실, "characters"의 정의가 모호하고, 문자열이 characters로 구성되어 있다고 정의하여 모호함을 해결하려는 시도는 실수입니다.

     

    유니코드, UTF-8 및 다국어 텍스트 처리의 세계에 대해 더 많이 말할 것이 있지만 다른 게시물을 기다릴 수 있습니다. 지금은 Go 문자열이 어떻게 작동하는지, 임의의 bytes를 포함 할 수 있지만 UTF-8이 디자인의 중심 부분임을 더 잘 이해하기를 바랍니다.

     

    원문 (Original Source): https://blog.golang.org/strings

     

    Strings, bytes, runes and characters in Go - The Go Blog

    Rob Pike 23 October 2013 Introduction The previous blog post explained how slices work in Go, using a number of examples to illustrate the mechanism behind their implementation. Building on that background, this post discusses strings in Go. At first, stri

    blog.golang.org