13. 문자열
13. 문자열
✅ 1. 문자열
- 문자열의 타입명은 string이며 큰따옴표 또는 백쿼트(그레이브)로 묶어서 표시한다
- 백쿼트로 묶으면 특수 문자가 동작하지 않으며 여러 줄에 걸쳐 문자열을 쓸 수 있다
1.1 UTF-8 문자코드
- Go는 UTF-8을 표준 문자코드로 사용한다
- 자주 사용되는 영문자, 숫자, 일부 특수문자는 1바이트로 표현하고 그 외 다른 문자들은 2 ~ 3바이트로 표현한다
1.2 rune 타입
- 문자 하나를 표현하는 데
rune
타입을 사용한다 - UTF-8 문자 하나를 표현하는데 최대 3바이트지만 Go 언어 기본 타입에서 3바이트 정수 타입은 제공되지 않기 때문에 4바이트 정수 타입
int32
의 별칭인 rune 타입을 사용한다
1
type rune int32 // rune 타입과 int32 타입은 이름만 다를 뿐 같은 타입이다
- rune 타입은 int32 타입과 동일하다
- rune 타입 변수를 그대로 출력하면 숫자로 출력된다
- 문자를 출력하기 위해서
%c
포맷을 사용한다
1
2
3
4
5
var char rune = '한'
fmt.Printf("%T\n", char) // int32
fmt.Println(char) // 54620
fmt.Printf("%c\n", char) // 한
1.3 문자열 길이
- len() 내장 함수 인자로 string 타입을 넣으면 문자열 길이가 아닌 메모리 크기를 반환한다
- string 타입과
[]rune
타입은 상호 타입 변환이 가능하며 len() 내장함수에[]rune
타입을 인자로 넣으면 글자 수를 알 수 있다
1
2
3
4
5
var str string = "한글"
runes := []rune(str)
fmt.Println(len(str)) // 6
fmt.Println(len(runes)) // 2
string 타입을
[]byte
타입으로 변환할 수 있다
✅ 2. 문자열 순회
2.1 인덱스를 사용한 바이트 단위 순회
1
2
3
4
5
6
7
8
9
10
11
12
13
var str string = "한글"
for i := 0; i < len(str); i++ {
fmt.Printf("타입: %T 문자: %c\n", str[i], str[i])
}
/*
타입: uint8 문자: í
타입: uint8 문자:
타입: uint8 문자:
타입: uint8 문자: ê
타입: uint8 문자: ¸
타입: uint8 문자:
*/
str[i]
와 같이 인덱스로 접근하면 바이트 단위로 순회한다
2.2 []rune
타입 변환 후 순회
1
2
3
4
5
6
7
8
9
10
var str string = "한글"
arr := []rune(str)
for i := 0; i < len(arr); i++ {
fmt.Printf("타입: %T 문자: %c\n", arr[i], arr[i])
}
/*
타입: int32 문자: 한
타입: int32 문자: 글
*/
[]rune
타입 변환 후 슬라이스를 순회하면 한 글자씩 순회할 수 있지만 변환 과정에서 불필요한 메모리 할당이 일어난다
2.3 range
키워드를 이용한 순회
1
2
3
4
5
6
7
8
9
var str string = "한글"
for _, v := range str {
fmt.Printf("타입: %T 문자: %c\n", v, v)
}
/*
타입: int32 문자: 한
타입: int32 문자: 글
*/
range
키워드를 이용해 index, value에 직접 접근하여 추가 메모리 할당 없이 순회할 수 있다
✅ 3. 문자열 연산
- 문자열을
+
,+=
연산자를 사용해서 이을 수 있다
1
2
3
str1 := "Hello"
str1 += " " + "World"
fmt.Println(str1) // Hello World
==
,!=
연산자를 통해 문자열의 값을 비교할 수 있다. 완전히 같다면true
, 아니라면false
이다
1
2
3
str1 := "Hello"
str3 := "Hello"
fmt.Println(str1 == str2) // true
- 대소 비교 연산자로 문자열 간 대소 비교가 가능하다. 문자열 앞 글자부터 UTF-8 코드에 따라 대소 비교한다
✅ 4. 문자열 구조
1
2
3
4
type StringHeader struct {
Data uintptr
Len int
}
- string 타입은 필드가 2개인 구조체로 구현되어있다
- Data 필드는 uintptr 타입으로 문자열 데이터가 있는 메모리 주소를 나타내는 포인터이다
- Len 필드는 int 타입으로 문자열의 길이(바이트 크기)를 나타낸다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func headerOf(s string) reflect.StringHeader {
// string → *reflect.StringHeader로 재해석
return *(*reflect.StringHeader)(unsafe.Pointer(&s))
}
func main() {
s1 := "Hello"
h1 := headerOf(s1)
fmt.Printf("Header.Data=%#x, Header.Len=%d\n", h1.Data, h1.Len)
s2 := s1
h2 := headerOf(s2)
fmt.Printf("Header.Data=%#x, Header.Len=%d\n", h2.Data, h2.Len)
}
// Header.Data=0x104f22279, Header.Len=5
// Header.Data=0x104f22279, Header.Len=5
- string도 구조체이므로 string 타입 간 대입이 발생하면 문자열 데이터가 복사되지 않고, Data와 Len이 복사된다.
- 문자열이 길더라도 변수끼리 대입 연산 시 16바이트 값만 복사된다(포인터 8바이트 + int 8바이트)
✅ 5. 문자열은 불변이다
- 문자열이 불변(immutable)하다는 말은 string 타입이 가리키는 문자열의 일부만 변경할 수 없다는 뜻이다
1
2
3
var str string = "Hello World"
str = "How are You?" // 가능
str[2] = 'a' // Error! 일부 바꾸기 불가능
[]byte
타입 변환을 할 때, 불변 원칙을 지키기 위해 문자열을 복사해서 새로운 메모리 공간을 만들어 슬라이스가 가리키도록 한다
1
2
3
4
5
6
7
var str string = "Hello World"
var slice []byte = []byte(str)
slice[2] = 'a'
fmt.Printf("%s\n", slice) // Healo World
fmt.Printf("%p\n", unsafe.StringData(str)) // 0x102816dc2
fmt.Printf("%p\n", unsafe.SliceData(slice)) // 0x1400000e0e0
- 문자열 합 연산 시, 기존 문자열의 메모리 공간을 건드리지 않고 새로운 공간에 문자열을 생성하기 때문에 연산 이후 메모리 주소가 변경된다
1
2
3
4
5
var str string = "Hello"
fmt.Printf("%p\n", unsafe.StringData(str)) // 0x10257a21c
str += " World"
fmt.Printf("%p\n", unsafe.StringData(str)) // 0x14000102030
- string 합 연산을 빈번하게 사용하는 경우
strings.Builder
를 사용하여 메모리 낭비를 줄일 수 있다
1
2
3
4
5
6
7
var builder strings.Builder
builder.WriteString("Hello")
builder.WriteRune(' ')
builder.WriteString("World")
fmt.Println(builder.String())
This post is licensed under CC BY 4.0 by the author.