Programming/Go

[Golang] 문자열

oompa-rumpa 2022. 12. 24. 16:53

Contents

    문자열

    문자열은 말 그대로 문자 집합이다.

    • 타입명은 string이다.
    • 문자열은 큰따옴표백쿼트(back quote)로 묶어서 표시한다.
      • 백쿼트는 그레이브라고도 부른다.
    • 백쿼트로 문자열을 묶으면 문자열 안의 특수문자가 일반 문자처럼 처리된다.
      • Shell에서의 홀따옴표랑 동일함.
    • 큰따옴표로는 한 줄만 묶을 수 있지만 백쿼트로 묶을 경우 여러 줄에 걸쳐서 문자열을 쓸 수 있다.

    예제 1. 백쿼트로 문자열을 묶으면 문자열 안의 특수문자가 일반 문자처럼 처리된다.

    package main
    
    import "fmt"
    
    func main() {
    	// ❶ 큰따옴표로 묶으면 특수 문자가 동작합니다.
    	str1 := "Hello\\t'World'\\n"
    
    	// ❷ 백쿼트로 묶으면 특수 문자가 동작하지 않습니다.
    	str2 := `Go is "awesome"!\\nGo is simple and\\t'powerful'`
    	fmt.Println(str1)
    	fmt.Println(str2)
    }
    
    ---
    Hello   'World'
    
    Go is "awesome"!\\nGo is simple and\\t'powerful'
    
    • 백쿼트로 묶은 str2는 특수문자가 동작하지 않고 그대로 출력되었다.

    예제 2. 큰따옴표로는 한 줄만 묶을 수 있지만 백쿼트로 묶을 경우 여러 줄에 걸쳐서 문자열을 쓸 수 있다.

    package main
    
    import "fmt"
    
    func main() {
    
    	// 큰따옴표에서 여러 줄을 표현하려면 \\n을 사용해야 합니다.
    	poet1 := "죽는 날까지 하늘을 우러러\\n한 점 부끄럼이 없기를,\\n잎새에 이는 바람에도\\n나는 괴로워했다.\\n"
    
    	// 백쿼트에서는 여러 줄 표현에 특수 문자가 필요 없습니다.
    	poet2 := `죽는 날까지 하늘을 우러러
    한 점 부끄럼이 없기를,
    잎새에 이는 바람에도
    나는 괴로워했다.`
    
    	fmt.Println(poet1)
    	fmt.Println(poet2)
    }
    ---
    죽는 날까지 하늘을 우러러
    한 점 부끄럼이 없기를,
    잎새에 이는 바람에도
    나는 괴로워했다.
    
    죽는 날까지 하늘을 우러러
    한 점 부끄럼이 없기를,
    잎새에 이는 바람에도
    나는 괴로워했다.
    

    1️⃣ UTF-8 문자코드

    Go는 UTF-8 문자코드를 표준 문자코드로 사용한다.

    • Go언어 창시자인 롭 파이크와 켄 톰슨이 고안한 문자코드이다.

    UTF-8의 특징

    1. 다국어 문자를 지원
    2. 자주 사용되는 영문자, 숫자, 일부 특수문자를 1 Byte로 표현하고 그 외 다른 문자들은 2~3 Byte로 표현한다.
      • UTF-16에 비해 크기를 절약할 수 있음.
    3. ANSI 코드와 1:1 대응이 되어 ANSI로 바로 변환할 수 있다.

    따라서 Go는 별다른 변환없이 한글이나 한자등을 사용할 수 있다.

    2️⃣ rune 타입으로 한 문자 담기

    UTF-8은 한 글자가 1~3 Byte 크기이기 때문에 UTF-8 문자값을 가지려면 최소 3 Byte가 필요하다. 그러나 Go언어의 기본 타입에서는 3 Byte의 정수타입이 제공되지 않는다.

    따라서 4 Byte 정수 타입인 rune 타입을 사용한다.

    • rune 타입은 int32 타입의 별칭 타입이다.
    • rune과 int32는 이름만 다른 뿐 같은 타입이다.

    형식

    type rune int32
    

    예제

    package main
    
    import "fmt"
    
    func main() {
    	var char rune = '한'
    
    	fmt.Printf("%T\\n", char) // ❶ char 타입 출력
    	fmt.Println(char)        // ❷ char값 출력
    	fmt.Printf("%c\\n", char) // ❸ 문자 출력
    }
    

    3️⃣ len()으로 문자열 크기 알아내기

    len() 내장 함수를 이용하여 문자열이 차지하는 메모리 크기를 알아낼 수 있다.

    예제

    package main
    
    import "fmt"
    
    func main() {
    	str1 := "가나다라마" // ❶ 한글 문자열
    	str2 := "abcde" // ❷ 영문 문자열
    
    	fmt.Printf("len(str1) = %d\\n", len(str1)) // 한글 문자열 크기
    	fmt.Printf("len(str2) = %d\\n", len(str2)) // 영문 문자열 크기
    }
    
    ---
    len(str1) = 15
    len(str2) = 5
    
    • 한글 문자열인 str1은 크기가 15이지만 영문 문자열인 st2는 크기가 5이다.
      • UTF-8에서 한글은 글자당 3 Byte를 차지하기 때문이다.
      • 영문은 1 Byte를 차지함.

    4️⃣ [ ]rune 타입 변환으로 글자 수 알아내기

    string 타입이자 rune 슬라이스 타입인 []rune 타입은 상호 타입 변환이 가능하다.

    예제 1

    package main
    
    import "fmt"
    
    func main() {
    	str := "Hello World"
    
    	// ❶ ‘H’, ‘e’, ‘l’, ‘l’, ‘o’, ‘ ‘, ‘W’, ‘o’, ‘r’, ‘l’, ‘d’ 문자코드 배열
    	runes := []rune{72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100}
    
    	fmt.Println(str)
    	fmt.Println(string(runes))
    }
    
    ---
    Hello World
    Hello World
    • “Hello World” 문자열은 ‘H’, ‘e’.. 등 문자들의 집합이고 각 문자들은 UTF-8 코드인 0x48, 0x65… 등의 값을 갖게된다.
      • 따라서 문자열은 각 문자의 코드값의 배열인 rune 배열로 나타낼 수 있다.
    • string 타입과 []rune 타입은 모두 문자들의 집합을 나타내므로 상호변환이 가능하다.

    예제 2

    package main
    
    import "fmt"
    
    func main() {
    	str := "hello 월드"    // ❶ 한글과 영문자가 섞인 문자열
    	runes := []rune(str) // ❷ []rune 타입으로 타입 변환
    
    	fmt.Printf("len(str) = %d\\n", len(str))     // ❸ string 타입 길이
    	fmt.Printf("len(runes) = %d\\n", len(runes)) // ➍ []rune 타입 길이
    }
    
    ---
    len(str) = 12
    len(runes) = 8
    
    • string 타입 변수 길이는 문자열의 바이트 길이가 반환된다.
    • 그러나 string → []rune 으로 타입 변환을 하면 각 글자들로 이루어진 배열로 변환된다.

    Go언어에서는 편의를 위하여 string, []rune 둘의 상호 타입 변환을 지원하고 있다.

    5️⃣ string 타입을 []byte로 타입 변환할 수 있다.

    []byte는 1 Byte 부호 없는 정수 타입의 가변 길이 배열이다.

    • 메모리는 1 Byte 단위로 저장되기 때문에 모든 문자열은 1 Byte 배열로 변환 가능하다.

    파일을 쓰거나 네트워크로 데이터를 전송하는 경우 io.Writer 인터페이스를 사용해야 하는데 io.Writer 인터페이스는 []byte 타입을 인수로 받기 때문에 []byte 타입으로 변환해야 한다.

    • 그래서 문자열을 쉽게 전송하고자 Go 언어에서는 string에서 []byte 타입으로 변환을 지원한다.

    6️⃣ 문자열 순회

    문자열에 들어있는 글자들을 순회하는 방법들을 아라보자.

    방법

    1. 인덱스를 사용한 Byte 단위의 순회
    2. []rune으로 타입 변환 후 한 글자씩 순회
    3. range 키워드를 이용한 한 글자씩 순회

    예제 1. 인덱스를 사용한 Byte 단위의 순회

    package main
    
    import "fmt"
    
    func main() {
    	str := "Hello 월드!" // ❶ 한영이 섞인 문자열
    
    	for i := 0; i < len(str); i++ { // ❶ 문자열 크기를 얻어 순회
    		// ❸ 바이트 단위로 출력
    		fmt.Printf("타입:%T 값:%d 문자값:%c\\n", str[i], str[i], str[i])
    	}
    }
    ---
    타입:uint8 값:72 문자값:H
    타입:uint8 값:101 문자값:e
    타입:uint8 값:108 문자값:l
    타입:uint8 값:108 문자값:l
    타입:uint8 값:111 문자값:o
    타입:uint8 값:32 문자값: 
    타입:uint8 값:236 문자값:ì
    타입:uint8 값:155 문자값:
    타입:uint8 값:148 문자값:
    타입:uint8 값:235 문자값:ë
    타입:uint8 값:147 문자값:
    타입:uint8 값:156 문자값:
    타입:uint8 값:33 문자값:!
    
    • str 문자열 만큼 인덱스를 사용하여 각 바이트 값들을 출력하는 for 문이다.
    • 영문과 공백은 제대로 출력하였으나 한글은 깨졌다.
    • 한글이 꺠지는 이유는 str[i] 처럼 인덱스로 접근하면 요소의 타입은 uint8, 즉 1 Byte이다. 따라서 3 Byte 크기인 한글은 깨지게 된다.

    예제 2. []rune으로 타입 변환 후 한 글자씩 순회

    package main
    
    import "fmt"
    
    func main() {
    	str := "Hello 월드!" // ❶ 한영 문자가 섞인 문자열
    	arr := []rune(str) // ❷ 문자열을 []rune으로 형변환
    
    	for i := 0; i < len(arr); i++ { // ❸ 문자열 크기를 얻어 순회
    		fmt.Printf("타입:%T 값:%d 문자값:%c\\n", arr[i], arr[i], arr[i])
    	}
    }
    ---
    타입:int32 값:72 문자값:H
    타입:int32 값:101 문자값:e
    타입:int32 값:108 문자값:l
    타입:int32 값:108 문자값:l
    타입:int32 값:111 문자값:o
    타입:int32 값:32 문자값: 
    타입:int32 값:50900 문자값:월
    타입:int32 값:46300 문자값:드
    타입:int32 값:33 문자값:!
    
    • 이렇게 []rune으로 변환한 다음에 순회하도록 하면 한 글자씩 순회할 수 있다.
    • 그러나 []rune으로 변환되는 과정에서 별도의 배열을 할당하기 때문에 불필요한 메모리를 사용하게 된다.

    예제 3. range 키워드를 이용한 한 글자씩 순회

    package main
    
    import "fmt"
    
    func main() {
    	str := "Hello 월드!"      // ❶ 한영 문자가 섞인 문자열
    	for _, v := range str { // ❷ range를 이용한 순회 (인덱스 값은 사용하지 않아 _로 무효화함)
    		fmt.Printf("타입:%T 값:%d 문자:%c\\n", v, v, v) // ❸ 출력
    	}
    }
    
    • 모든 문자 타입이 int3, 즉 rune이다.
    • 이처럼 range를 이용하면 추가 메모리 할당 없이 문자열을 순회할 수 있어서 메모리 낭비를 없앨 수 있다.

    문자열 합치기

    문자열은 +와 += 연산을 사용하여 문자열을 이을 수 있다.

    예제 1.

    package main
    
    import "fmt"
    
    func main() {
    	str1 := "Hello"
    	str2 := "World"
    
    	str3 := str1 + " " + str2 //❶ str1, " ", str2를 잇습니다.
    	fmt.Println(str3)
    
    	str1 += " " + str2 // ❷ str1에 " " + str2 문자열을 붙입니다.
    	fmt.Println(str1)
    }
    

    1️⃣ 문자열 비교하기

    연산자 (==, !=)를 사용하여 문자열이 같은지 같지 않은지 서로 비교해보자.

    예제 1.

    package main
    
    import "fmt"
    
    func main() {
    	str1 := "Hello"
    	str2 := "Hell"
    	str3 := "Hello"
    
    	fmt.Printf("%s == %s : %v\\n", str1, str2, str1 == str2)
    	fmt.Printf("%s != %s : %v\\n", str1, str2, str1 != str2)
    	fmt.Printf("%s == %s : %v\\n", str1, str3, str1 == str3)
    	fmt.Printf("%s != %s : %v\\n", str1, str3, str1 != str3)
    }
    

    2️⃣ 문자열 대소 비교하기 :>, <, <=, >=

    문자열 대소 비교는 첫 글자부터 하나씩 값을 비교해서 그 글자에 해당하는 유니코드 값이 다를 경우 대소를 반환한다.

    예제

    package main
    
    import "fmt"
    
    func main() {
    	str1 := "BBB"
    	str2 := "aaaaAAA"
    	str3 := "BBAD"
    	str4 := "ZZZ"
    
    	fmt.Printf("%s > %s : %v\\n", str1, str2, str1 > str2)   // ❶
    	fmt.Printf("%s < %s : %v\\n", str1, str3, str1 < str3)   // ❷
    	fmt.Printf("%s <= %s : %v\\n", str1, str4, str1 <= str4) // ❸
    }
    
    ---
    BBB > aaaaAAA : false
    BBB < BBAD : false
    BBB <= ZZZ : true
    
    • B의 UTF-8 값이 66번이고 a는 97번이기 때문에 a가 더 큰 값이다.
      • 그래서 BBB > aaaaAAA는 false인 것이다.
    • 문자열 대소 비교 시 문자열 길이와 상관없이 앞글자부터 비교한다.

    문자열은 불변이다.

    string 타입이 가리키는 문자열의 일부만 변경할 수 없다.

    예제 1.

    package main
    
    import "fmt"
    
    func main() {
    	var str string = "Hello World"
    	var slice []byte = []byte(str) // ❶ 슬라이스로 타입 변환
    
    	slice[2] = 'a' // ❷ 3번째 문자 변경
    
    	fmt.Println(str)
    	fmt.Printf("%s\\n", slice)
    }
    
    • str과 slice가 가리키는 메모리 공간은 서로 다르다.
    • Go언어는 슬라이스로 타입 변환을 할 때 문자열을 복사하여 새로운 메모리 공간을 만들어 슬라이스가 가리키도록 한다.
      • 그래야 문자열 불변 원칙을 지킬 수 있다.

    왜 문자열 불변 원칙이 필요한가?

    왜 Go언어에서는 빈번한 합 연산과 메모리가 낭비되는 데도 왜 문자열 불변 원칙을 지키려고 할까?

    가장 큰 이유는 예기치 못한 버그를 방지하기 위해서이다.

     

    요약

    1. 문자열은 문자의 집합이고 타입명은 string이다.
    2. 문자열은 큰따옴표나 백쿼트로 묶어서 표현한다.
    3. UTF-8 문자코드를 사용하여 문자열을 표현한다.
    4. range를 이용하여 글자 단위로 순회할 수 있다.
    5. +로 문자열을 합치고 사전식으로 대소 비교한다.
    6. 문자열 내부는 포인터와 길이 필드로 구성된다.