Golang 기초 (11) : 문자열에 대하여

Eon Kim·2022년 2월 27일
2

Golang 기초 

목록 보기
11/14
post-thumbnail

안녕하세요, 주니어 개발자 Eon입니다.
이번 포스트는 문자열에 대한 내용입니다.


📝 문자열 (string)

문자의 집합이자 문자들로 이루어진 배열입니다.
프로그래밍을 처음 시작할 때 출력하는 "Hello World!!" 또한 문자열입니다.
'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '!', '!'
위의 문자들이 모여, 하나의 문자열을 구성한 것입니다.
golang의 자료형 중, stringrune으로 표현할 수 있습니다.


📌 유니코드

ASCII code (0~255; 1byte)로는 표현할 수 있는 문자에 한계가 있었습니다.
https://www.ascii-code.com/
그래서 개발된 게 유니코드입니다.
유니코드는 국제 표준이며, 여러 가지 종류가 있습니다. (utf-1, utf-7, utf-8, utf-EBCDIC, utf-16, utf-32)

  • utf-16
    • 문자 하나를 표현하는 데에 2 바이트가 필요합니다.
    • 장점 : 모든 문자를 2바이트로 표현 가능합니다.
    • 단점 : 1바이트로 표현 가능한 문자도 2바이트를 차지합니다.
  • utf-8 (golang에서 사용되는 문자 인코딩 방식)
    • 문자 하나를 표현하는 데에 1 ~ 4 바이트가 필요합니다.
    • 장점 : ASCII 1바이트 문자들로 구성을 많이 할수록 메모리를 적게 사용합니다.
    • 단점 : 그 외의 문자들의 사용이 많아질수록 메모리 사용량이 커집니다.

📍 utf-8

인터넷 공간의 데이터 중 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)에서 초기화한 값에 개행을 포함하여 초기화합니다. 개행은 마지막 줄을 제외한 모든 줄에 포함돼 있습니다.


📌 문자열의 순회

문자열은 반복문 제어를 통해 그 안의 문자들을 순회할 수 있습니다.
방법은 아래와 같습니다.


📍 len()

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 for문 순회 (영어)

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문 순회를 했을 때, 아무런 문제 없이 원했던 결과를 얻을 수 있었습니다.


📍 C-like 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개라는 것을 유추할 수 있습니다.


📍 for-range 순회 (영어)

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로 순회되는 모습을 볼 수 있습니다.
단, 한 가지 차이점이 있다면 Typeint32로 출력되는 모습을 볼 수 있습니다.


📍 for-range : string -> rune

rangestring을 넣으면 rune 타입으로 변환하여 순회를 하기 때문에 그렇습니다.
golang for-range의 특징이라고 보시면 됩니다.
rune타입은 string처럼 바이트 단위로 쪼개지 않고도 문자 하나를 온전히 표현하는 4바이트의 크기를 가지고 있기 때문에 온전히 모든 문자의 표현이 가능합니다.


📍 for-range 순회 (한글)

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로 순회를 했더니 한글도 문제없이 출력되는 모습을 확인할 수 있습니다.


📍 C-like for문 순회 (한글); []rune

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가 출력된 것을 볼 수 있습니다.
문자로 이루어진 배열이라 했지만 길이가 서로 다른 문자열끼리도 대입 연산이 가능합니다.


📍 문자열의 연산 : 비교 연산

문자열은 문자들로 이루어진 배열입니다.
각각의 요소는 문자이며, golangutf-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

문자열비교 연산은 각 문자의 문자 코드 단위로 이루어집니다.
Aa를 비교하고, Bb, Cc 순으로 비교합니다.
조건문 str1 > str2의 비교를 하면, 맨 처음 비교하게 되는 Aa의 비교에서 이미 조건을 만족하지 않기 때문에 뒤에 오는 문자의 비교는 하지 않습니다.
바로 다음 조건문으로 넘어가, 모든 문자열 요소들에 대해 조건을 만족하는지 확인합니다.



📝 문자열 덧셈, 대입 연산 시, 메모리의 동작

문자열은 문자의 배열이라고 했습니다.
golang에서는 배열의 크기가 한 번 정해지면 변경이 불가합니다.
문자열덧셈 연산이 가능하다고 해서 한 번 정한 크기를 변경할 수 있는 것은 아닙니다.
내부 동작에서 메모리 할당을 알아서 해주니 사용자는 신경 쓸 필요가 없습니다.
다만, 알고는 있어야 메모리 사용량을 줄일 수 있을 것입니다.


📌 문자열 변수의 실제 메모리 크기

string 변수 str를 선언하고 초기화를 하면 실제로 할당되는 메모리의 크기는 다음과 같습니다.

len(str)+ unsafe.Sizeof(reflect.StringHeader{})
문자열 바이트 길이 + 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 타입만으로는 불가능합니다.
문자열 일부 변경은 타입 변환을 통해서 수행가능합니다.


📌 string to rune (or byte), rune (or byte) to 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의 크기


📍 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으로 변환 후에 출력하니, 값이 바뀐 것을 확인할 수 있었습니다.



이상 문자열에 대한 내용이었습니다.
감사합니다.👍

profile
주니어 개발자

0개의 댓글