안녕하세요, 주니어 개발자 Eon입니다.
이번 포스트는 문자열에 대한 내용입니다.
문자의 집합이자 문자들로 이루어진 배열
입니다.
프로그래밍을 처음 시작할 때 출력하는 "Hello World!!"
또한 문자열
입니다.
'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '!', '!'
위의 문자들이 모여, 하나의 문자열
을 구성한 것입니다.
golang
의 자료형 중, string
과 rune
으로 표현할 수 있습니다.
ASCII code
(0~255; 1byte)로는 표현할 수 있는 문자에 한계가 있었습니다.
https://www.ascii-code.com/
그래서 개발된 게 유니코드
입니다.
유니코드
는 국제 표준이며, 여러 가지 종류가 있습니다. (utf-1, utf-7, utf-8, utf-EBCDIC, utf-16, utf-32)
utf-16
utf-8
(golang
에서 사용되는 문자 인코딩 방식)ASCII
1바이트 문자들로 구성을 많이 할수록 메모리를 적게 사용합니다.인터넷 공간의 데이터 중 80%를 영어와 숫자가 차지한다고 합니다.
이러한 점에서 utf-8
은 매우 효율적인 인코딩 방식이라고 볼 수 있습니다.
모든 문자에 대하여 같은 메모리가 아닌, 모든 문자에 각각 필요로 하는 만큼 메모리를 할당합니다.
1바이트 : ASCII
0~127 총 128개
2바이트 : 그리스어, 키릴어, 콥트어, 아르메니아어, 히브리어, 아랍어, 시리아어, 타나 문자, 분음 부호 결합 총 1920개
3바이트 : 대부분의 중국어, 일본어, 한국어
4바이트 : 다른 평면 문자, 일반적이지 않은 한중일어, 수학 기호, 이모지 등이 포함
한글의 자음, 모음, 받침을 합쳐서 3바이트인 것인가하는 추측은 하지 말아야 합니다.
전혀 그렇지 않고, 그저 국제 표준일 뿐입니다.
var str string var str string = "Hello World!!" var str string = `Hello World!!` var str string = ` Hello World!! `
문자열
의 선언은 위의 방법 네 가지가 있습니다.
1) 초기화 없이 선언합니다. 이 때, 값은""
빈문자열
이 됩니다.
2)Hello World!!
로 초기화를 합니다. 동일한 줄에서 큰 따옴표를 닫아야 합니다.
3) ` `로 초기화합니다.
4) 3)에서 초기화한 값에 개행을 포함하여 초기화합니다. 개행은 마지막 줄을 제외한 모든 줄에 포함돼 있습니다.
문자열은 반복문 제어를 통해 그 안의 문자들을 순회할 수 있습니다.
방법은 아래와 같습니다.
golang
의 기본 built-in
함수입니다.
string
타입을 넣으면 string
변수의 바이트 길이가 반환됩니다.
builtin.go
에서 len()
에 대하여 다음과 같은 설명을 포함합니다.
// The len built-in function returns the length of v, according to its type:
// Array: the number of elements in v.
// Pointer to array: the number of elements in *v (even if v is nil).
// Slice, or map: the number of elements in v; if v is nil, len(v) is zero.
// String: the number of bytes in v.
// Channel: the number of elements queued (unread) in the channel buffer;
// if v is nil, len(v) is zero.
// For some arguments, such as a string literal or a simple array expression, the
// result can be a constant. See the Go language specification's "Length and
// capacity" section for details.
func len(v Type) int
C-like 순회는 바이트 단위로 순회합니다.
var str string = "Hello Wolrd!!" for i := 0; i < len(str); i++ { fmt.Printf("Type : %T, Character : %c, Code : %d\n", str[i], str[i], str[i]) } /* OUTPUT Type : uint8, Character : H, Code : 72 Type : uint8, Character : e, Code : 101 Type : uint8, Character : l, Code : 108 Type : uint8, Character : l, Code : 108 Type : uint8, Character : o, Code : 111 Type : uint8, Character : , Code : 32 Type : uint8, Character : W, Code : 87 Type : uint8, Character : o, Code : 111 Type : uint8, Character : l, Code : 108 Type : uint8, Character : r, Code : 114 Type : uint8, Character : d, Code : 100 Type : uint8, Character : !, Code : 33 Type : uint8, Character : !, Code : 33 */
string
은 문자들의 배열입니다.
그리고 C-like for문은 바이트 단위로 순회합니다.
"Hello World!!"
안의 모든 문자는 각각 1바이트입니다.
따라서 위와 같이 for문 순회를 했을 때, 아무런 문제 없이 원했던 결과를 얻을 수 있었습니다.
var 스트링 string = "헬로 월드!!" for i := 0; i < len(스트링); i++ { fmt.Printf("Type : %T, Character : %c, Code : %d\n", 스트링[i], 스트링[i], 스트링[i]) } /* OUTPUT Type : uint8, Character : í, Code : 237 Type : uint8, Character : , Code : 151 Type : uint8, Character : ¬, Code : 172 Type : uint8, Character : ë, Code : 235 Type : uint8, Character : ¡, Code : 161 Type : uint8, Character : , Code : 156 Type : uint8, Character : , Code : 32 Type : uint8, Character : ì, Code : 236 Type : uint8, Character : ode : 155 Type : uint8, Character : , Code : 148 Type : uint8, Character : ë, Code : 235 Type : uint8, Character : , Code : 147 Type : uint8, Character : , Code : 156 Type : uint8, Character : !, Code : 33 Type : uint8, Character : !, Code : 33 */
앞서 설명한 바와 같이,
utf-8
에서 한글은 글자 하나 당 3바이트로 구성됩니다.
C-like for문 순회는len()
을 사용해야 하는데,len()
은 바이트 길이를 반환합니다.
그리고 C-like for문은 바이트 길이만큼 순회를 하기 때문에 한글 글자 하나를 구성하는 3개 바이트 중 하나씩만 읽을 수 있습니다.
때문에 바이트 순회로는 한글을 온전히 읽지 못합니다.
위의 결과에서 사이에 공백과 맨 아래 느낌표를 제외하고 나머지 부분에서 3개 바이트 당 한글 글자 1개라는 것을 유추할 수 있습니다.
var str string = "Hello Wolrd!!" for _, v := range str { fmt.Printf("Type : %T, Character : %c, Code : %d\n", v, v, v) } /* OUTPUT Type : int32, Character : H, Code : 72 Type : int32, Character : e, Code : 101 Type : int32, Character : l, Code : 108 Type : int32, Character : l, Code : 108 Type : int32, Character : o, Code : 111 Type : int32, Character : , Code : 32 Type : int32, Character : W, Code : 87 Type : int32, Character : o, Code : 111 Type : int32, Character : l, Code : 108 Type : int32, Character : r, Code : 114 Type : int32, Character : d, Code : 100 Type : int32, Character : !, Code : 33 Type : int32, Character : !, Code : 33 */
영어는 무리 없이
range
로 순회되는 모습을 볼 수 있습니다.
단, 한 가지 차이점이 있다면Type
이int32
로 출력되는 모습을 볼 수 있습니다.
range
에string
을 넣으면rune
타입으로 변환하여 순회를 하기 때문에 그렇습니다.
golang
for-range
의 특징이라고 보시면 됩니다.
rune
타입은string
처럼 바이트 단위로 쪼개지 않고도 문자 하나를 온전히 표현하는 4바이트의 크기를 가지고 있기 때문에 온전히 모든 문자의 표현이 가능합니다.
var 스트링 string = "헬로 월드!!" for _, v := range 스트링 { fmt.Printf("Type : %T, Character : %c, Code : %d\n", v, v, v) } /* OUTPUT Type : int32, Character : 헬, Code : 54764 Type : int32, Character : 로, Code : 47196 Type : int32, Character : , Code : 32 Type : int32, Character : 월, Code : 50900 Type : int32, Character : 드, Code : 46300 Type : int32, Character : !, Code : 33 Type : int32, Character : !, Code : 33 */
for-range
로 순회를 했더니 한글도 문제없이 출력되는 모습을 확인할 수 있습니다.
var 스트링 string = "헬로 월드!!" var 룬슬라이스 []rune = []rune(스트링) for i := 0; i < len(룬슬라이스); i++ { fmt.Printf("Type : %T, Character : %c, Code : %d\n", 룬슬라이스[i], 룬슬라이스[i], 룬슬라이스[i]) } /* OUTPUT Type : int32, Character : 헬, Code : 54764 Type : int32, Character : 로, Code : 47196 Type : int32, Character : , Code : 32 Type : int32, Character : 월, Code : 50900 Type : int32, Character : 드, Code : 46300 Type : int32, Character : !, Code : 33 Type : int32, Character : !, Code : 33 */
len()
은 위에 써있듯이슬라이스
에 대해서는 요소의 개수를 반환합니다.
따라서[]rune
의 요소인rune
타입의 값들을 출력합니다.
문자열
은 연산이 가능합니다.
단, 사칙연산 중에는 덧셈만 가능하고, 대입 연산이나 비교 연산이 가능합니다.
덧셈 연산이 가능합니다.
var str1 string = "Hello" var str2 string = "World" var strMerge string = str1 + " " + str2 + "!!" fmt.Println(strMerge) // Hello World!!
위와 같이
"Hello"
라는문자열
과"World!!"
라는문자열
을 하나의문자열
로 합치는 게 가능합니다.
대입 연산이 가능합니다.
var str1 string = "Hello" var str2 string = "" str2 = str1 fmt.Println(str2) // Hello
str2
는 빈 문자열로 초기화를 했다가,str1
을 대입했습니다.
str2
의 값으로Hello
가 출력된 것을 볼 수 있습니다.
문자로 이루어진 배열이라 했지만 길이가 서로 다른 문자열끼리도 대입 연산이 가능합니다.
문자열
은 문자들로 이루어진 배열입니다.
각각의 요소는 문자이며, golang
은 utf-8
을 지원하고 utf-8
은 모든 문자를 4byte 내로 표현할 수 있습니다.
golang
에서 문자는 rune
타입이며 rune
타입은 utf-8
의 모든 문자를 표현할 수 있는 4바이트 크기의 int32
의 별칭 타입입니다.
그래서 문자 코드는 정수로 표현할 수 있습니다.
문자열의 비교 연산은 문자 코드의 비교입니다.
var str1 string = "ABCDE" var str2 string = "abcde" if str1 > str2 { fmt.Println("str1 > str2") } else if str1 < str2 { fmt.Println("str1 < str2") } // str1 < str2
문자열
의 비교 연산은 각 문자의 문자 코드 단위로 이루어집니다.
A
와a
를 비교하고,B
와b
,C
와c
순으로 비교합니다.
조건문str1 > str2
의 비교를 하면, 맨 처음 비교하게 되는A
와a
의 비교에서 이미 조건을 만족하지 않기 때문에 뒤에 오는 문자의 비교는 하지 않습니다.
바로 다음 조건문으로 넘어가, 모든 문자열 요소들에 대해 조건을 만족하는지 확인합니다.
문자열
은 문자의 배열이라고 했습니다.
golang
에서는 배열의 크기가 한 번 정해지면 변경이 불가합니다.
문자열
의 덧셈 연산이 가능하다고 해서 한 번 정한 크기를 변경할 수 있는 것은 아닙니다.
내부 동작에서 메모리 할당을 알아서 해주니 사용자는 신경 쓸 필요가 없습니다.
다만, 알고는 있어야 메모리 사용량을 줄일 수 있을 것입니다.
string
변수 str
를 선언하고 초기화를 하면 실제로 할당되는 메모리의 크기는 다음과 같습니다.
len(str)+ unsafe.Sizeof(reflect.StringHeader{})
문자열 바이트 길이 + StringHeader의 크기
문자열
에 대한 정보를 담고 있습니다.
type StringHeader struct { Data uintptr Len int }
Data
: 실제 값이 저장된 메모리 주소
Len
: 문자열의 길이
var str1 string = "Hello" var str2 string = "World" str2 += str1 var str3 string = str2 strHeader1 := (*reflect.StringHeader)(unsafe.Pointer(&str1)) strHeader2 := (*reflect.StringHeader)(unsafe.Pointer(&str2)) strHeader3 := (*reflect.StringHeader)(unsafe.Pointer(&str3)) fmt.Println("Value of strHeader1 :", strHeader1) fmt.Println("Value of strHeader2 :", strHeader2) fmt.Println("Value of strHeader3 :", strHeader3) fmt.Printf("Pointer value of strHeader1 : %p\n", strHeader1) fmt.Printf("Pointer value of strHeader2 : %p\n", strHeader2) fmt.Printf("Pointer value of strHeader3 : %p\n", strHeader3) /* OUTPUT Value of strHeader1 : &{4807070 5} Value of strHeader2 : &{824634400768 10} Value of strHeader3 : &{824634400768 10} Pointer value of strHeader1 : 0xc00008a210 Pointer value of strHeader2 : 0xc00008a220 Pointer value of strHeader3 : 0xc00008a230 */
위의
Pointer value
결과를 보면, 각str1
,str2
,str3
의 주소값은 모두 다른 것을 볼 수 있으며, 그 차이가 16바이트인 것을 확인할 수 있습니다.
위 그림과 같이str1
,str2
,str3
변수가 선언되고 초기화될 때마다 16바이트씩 메모리가 할당된 것을 볼 수 있습니다.
문자열
을 더하든, 대입하든string
변수 자체는 16바이트 크기의StringHeader
를 가지기 때문에 이러한 연산이 가능합니다.
단, 유동적으로string
변수의 크기를 줄였다가 늘렸다가 할 수 있는 것이 아니기 때문에 연산을 할 때마다 새로 메모리를 할당한다는 단점이 있습니다.
string
타입만으로는 불가능합니다.
문자열 일부 변경은 타입 변환을 통해서 수행가능합니다.
var str string = "a" var slice []rune = []rune(str) // var slice []byte = []byte(str)
문자열
의 일부를 변경하기 위해서는 먼저[]rune
or[]byte
타입으로 변환해야 합니다.
위와 같이 문자 1개로만 이루어진string
변수도 마찬가지입니다.
str = string(slice)
[]run
타입의 변수를string
으로 변환하기 위해서는 위와 같이 작성하면 됩니다.
slice
변수 slice
를 선언하고 초기화를 하면 실제로 할당되는 메모리의 크기는 다음과 같습니다.
len(slice)+ unsafe.Sizeof(reflect.SliceHeader{})
슬라이스 바이트 길이 + SliceHeader의 크기
슬라이스
에 대한 정보를 담고 있습니다.
type SliceHeader struct { Data uintptr Len int Cap int }
Data
: 실제 값이 저장된 메모리 주소
Len
: 슬라이스의 길이
Cap
: 슬라이스의 용량 (슬라이스의 길이를 늘릴 경우, Capacity를 넘지 않으면 그냥 붙여서 늘리고, Capacity를 넘어서면 새로운 슬라이스를 기존의 용량의 두 배로 만듭니다.)
var str string = "hello world" var runeSlice []rune = []rune(str) fmt.Println("Original string value :", str) strHeader := (*reflect.StringHeader)(unsafe.Pointer(&str)) fmt.Println("Value of strHeader :", strHeader) fmt.Printf("Pointer value of strHeader : %p\n\n", strHeader) for i, v := range runeSlice { if v == 'o' { runeSlice[i] = '!' } } sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&runeSlice)) fmt.Println("Value of sliceHeader :", sliceHeader) fmt.Println("Size of Slice :", unsafe.Sizeof(runeSlice)) fmt.Println() str = string(runeSlice) fmt.Println("Changed string value :", str) fmt.Println("Value of strHeader :", strHeader) fmt.Printf("Pointer value of strHeader : %p\n", strHeader) /* OUTPUT Original string value : hello world Value of strHeader : &{4815093 11} Pointer value of strHeader : 0xc000010240 Value of sliceHeader : &{824633811136 11 12} Size of Slice : 24 Changed string value : hell! w!rld Value of strHeader : &{824633819312 11} Pointer value of strHeader : 0xc000010240 */
string
타입의 변수str
을[]rune
타입으로 변환한 뒤, 순회를 합니다.
순회 중, 알파벳o
를 만나면 해당[]rune
에!
로 값을 바꾸도록 했습니다.
다시[]rune
타입을string
으로 변환 후에 출력하니, 값이 바뀐 것을 확인할 수 있었습니다.
이상 문자열에 대한 내용이었습니다.
감사합니다.👍