[Golang] 문자열
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 Byte로 표현하고 그 외 다른 문자들은 2~3 Byte로 표현한다.
- UTF-16에 비해 크기를 절약할 수 있음.
- 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 으로 타입 변환을 하면 각 글자들로 이루어진 배열로 변환된다.
5️⃣ string 타입을 []byte로 타입 변환할 수 있다.
[]byte는 1 Byte 부호 없는 정수 타입의 가변 길이 배열이다.
- 메모리는 1 Byte 단위로 저장되기 때문에 모든 문자열은 1 Byte 배열로 변환 가능하다.
파일을 쓰거나 네트워크로 데이터를 전송하는 경우 io.Writer 인터페이스를 사용해야 하는데 io.Writer 인터페이스는 []byte 타입을 인수로 받기 때문에 []byte 타입으로 변환해야 한다.
- 그래서 문자열을 쉽게 전송하고자 Go 언어에서는 string에서 []byte 타입으로 변환을 지원한다.
6️⃣ 문자열 순회
문자열에 들어있는 글자들을 순회하는 방법들을 아라보자.
방법
- 인덱스를 사용한 Byte 단위의 순회
- []rune으로 타입 변환 후 한 글자씩 순회
- 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언어에서는 빈번한 합 연산과 메모리가 낭비되는 데도 왜 문자열 불변 원칙을 지키려고 할까?
가장 큰 이유는 예기치 못한 버그를 방지하기 위해서이다.
요약
- 문자열은 문자의 집합이고 타입명은 string이다.
- 문자열은 큰따옴표나 백쿼트로 묶어서 표현한다.
- UTF-8 문자코드를 사용하여 문자열을 표현한다.
- range를 이용하여 글자 단위로 순회할 수 있다.
- +로 문자열을 합치고 사전식으로 대소 비교한다.
- 문자열 내부는 포인터와 길이 필드로 구성된다.