안녕하세요 ㅎㅎ 고랭 스터디입니다.
이번 주에는 15장 문자열, 16장 패키지를 배워보았습니다.
특히 Go는 문자열을 다루는 방식이 특이해서, 저희 모두 스터디에서 신기해했던 기억이 나는데요~!
자세한 내용은 아래 정리를 참고해주세요 :)
Chapter 15. String
15.1 문자열
Go의 문자열 출력 방법은 2가지가 있다.
- 큰따옴표
- 백쿼트
package main
import "fmt"
func main() {
// 1) 큰따옴표로 묶기
str1 := "Hello\\t'World'\\n"
poet1 := "죽는 날까지 하늘을 우러러\\n한 점 부끄럼이 없기를\\n"
// 2) 백쿼트로 묶기
str2 := `Hello\\t"Go"\\nWorld!`
poet2 := `죽는 날까지 하늘을 우러러
한 점 부끄럼이 없기를
잎새에 이는 바람에도
나는 괴로워했다.`
fmt.Println(str1)
fmt.Println(str2)
fmt.Println()
fmt.Println(poet1)
fmt.Println(poet2)
}
→ 결과
백쿼트로 묶을 경우, 특수문자인 \t \n이 그대로 출력된다.
또한 백쿼트는 여러 줄에 걸쳐서 문자열을 쓸 수 있지만, 큰따옴표는 한 줄에만 쓸 수 있어 개행하려면 \n을 사용해야 한다.
Hello 'World'
Hello\\t"Go"\\nWorld!
죽는 날까지 하늘을 우러러
한 점 부끄럼이 없기를
죽는 날까지 하늘을 우러러
한 점 부끄럼이 없기를
잎새에 이는 바람에도
나는 괴로워했다.
📌 문자 한 개 담기; rune 타입
문자 하나를 표현하는 데 rune 타입을 사용한다.
rune 타입은 int32 타입의 별칭 타입이다. 즉, 이름만 다를 뿐 int32와 성질이 같다.
문자를 표현하기 위해 별칭 rune을 사용한다고 이해하자.
package main
import "fmt"
func main() {
var ch1 rune = '음'
fmt.Printf("%T\\n", ch1) // ch1의 타입 출력
fmt.Println(ch1) // ch1의 값 출력
fmt.Printf("%c\\n", ch1) // ch1에 저장된 값을 문자로 출력
}
→ 결과
rune으로 정의한 문자 ch1의
- 타입 %T 를 출력하면 → int32
- 값을 출력하면 → 51020 (int32 타입이므로 값은 숫자가 나온다)
- 문자 %c 을 출력하면 → 음 (문자 그대로)
int32
51020
음
📌 문자열 크기 알아내기; len()
len()을 통해 출력되는 '문자열 크기'는 말 그대로 문자열이 차지하는 메모리 크기이다.
한글은 한 글자 당 3 byte, 영어는 한 글자 당 1 byte를 차지한다.
package main
import "fmt"
func main() {
str1 := "가나다라마"
str2 := "abcde"
fmt.Printf("'가나다라마'의 크기= %d\\n", len(str1)) //한글 문자열 크기
fmt.Printf("'abcde'의 크기= %d\\n", len(str2)) //영문 문자열 크기
}
→ 결과
'가나다라마'의 크기= 15
'abcde'의 크기= 5
📌 글자 수 알아내기; []rune 타입 변환
[]rune 타입은 상호 타입 변환이 가능하다. (슬라이스 타입이라서, 이는 18장에서 설명)
rune 배열의 각 요소에는 문자열의 각 글자가 대입된다.
[]rune 타입을 이용해 문자열의 글자 수를 알아내는 방법은
- 타입 변환 (string을 []rune 타입으로) runes := []rune(str)
- len(runes) ⇒ 글자 수가 반환된다.
package main
import "fmt"
func main() {
str := "Hello 월드"
runes := []rune(str) // 문자열을 []rune 타입으로 변환
fmt.Printf("len(str) = %d\\n", len(str)) // 메모리 크기 반환
fmt.Printf("len(runes) = %d\\n", len(runes)) //글자수 반환; len(runes)
}
→ 결과
len(str) = 12
len(runes) = 8
✔ len(**[]rune(str)**) 이렇게도 가능
💡 또한, String 타입을 []byte 로 타입 변환할 수 있다.
→ 20장 '인터페이스' 와 A.3절 '입출력 처리'에서 다룬다.
지금은 string과 []byte 타입 간 상호 변환이 가능하다는 것만 알고 넘어가자.
15.2 문자열 순회
- 인덱스를 사용한 바이트 단위 순회
- **[]rune** 으로 변환 후 한 글자씩 순회
- range 를 이용해 한 글자씩 순회
1. 인덱스를 사용한 바이트 단위 순회하기
str := "Hello 월드!"
for i := 0; i < len(str); i++ {
fmt.Printf("타입: %T\\t값:%d\\t문자값:%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 문자값:!
- 인덱스를 사용해 각 바이트값을 출력한다.
- 한글이 깨진다. 왜? 영어는 1byte이므로 잘 출력되지만, 한글은 3byte이므로 깨진다.
2. []rune 으로 타입 변환 후 한 글자씩 순회하기
str := "Hello 월드!"
arr := []rune(str)
for i := 0; i < len(arr); i++ {
fmt.Printf("타입: %T\\t값:%d\\t문자값:%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 으로 변환하는 과정에서 불필요한 메모리 할당(arr 정의)이 일어난다.
- 이를 해결하려면, range 를 사용하자.
3. range 키워드 사용하여 한 글자씩 순회
str := "Hello 월드!"
for _, v := range str {
fmt.Printf("타입: %T\\t값:%d\\t문자값:%c\\n", v, v, v)
}
→ 결과
타입: int32 값:72 문자값:H
타입: int32 값:101 문자값:e
타입: int32 값:108 문자값:l
타입: int32 값:108 문자값:l
타입: int32 값:111 문자값:o
타입: int32 값:32 문자값:
타입: int32 값:50900 문자값:월
타입: int32 값:46300 문자값:드
타입: int32 값:33 문자값:!
- 이처럼 range를 이용하면, 추가 메모리 할당 없이 문자열을 한 글자씩 순회할 수 있다.
- 모든 문자 타입이 int32, 즉 rune 이다.
- 한글, 영어 모두 잘 출력 된다.
15.3 문자열 합치기
+와 += 연산을 사용해서 문자열을 이을 수 있다.
package main
import "fmt"
func main() {
str1 := "Hello"
str2 := "World"
str3 := str1 + " " + str2
fmt.Println(str3)
str1 += " " + str2 // str1 + " " + str2 와 동일
fmt.Println(str1)
}
📌 문자열 비교하기
연산자 == , != 을 사용해서 두 문자열이 같은지 비교할 수 있다.
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)
}
📌 문자열 대소 비교하기
> , < , >=, <= 연산자를 이용해서 문자열 간 대소 비교를 할 수 있다.
문자열 대소 비교는 첫 글자부터 하나씩 값을 비교해서, 그 글자의 유니코드 값이 다를 경우 대소를 반환한다.
즉, 첫 글자가 더 큰 값이면, 더 큰 문자열이다.
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
15.4 문자열 구조
문자열을 사용하는 데 있어서 string 자료구조까지 알 필요는 없다. 그러나 알아두면 좋기 때문에(^^...) 알아보도록 하자.
string 타입의 구조 살펴보기
type StringHeader struct {
Data uintptr
Len int
}
이와 같이 string은 필드가 2개인 구조체이다.
- Data: uintptr 타입, 즉 string이 메모리 상에서 위치해있는 주소를 나타내는 포인터 타입
- Len: 문자열의 길이를 저장함
왜 이렇게 구성되어있는가?
str2 := str1
위와 같이 string끼리 대입하는 경우에, 똑같은 문자열을 복사하는 것이 아니라, str1의 Data와 Len 값만 str2에 복사하는 것이다.
즉, 문자열 자체는 복사되지 않고, 두 문자열이 같은 메모리 주소를 가리키게 되는 것.
15.5 '문자열 불변 법칙'
문자열은 불변이다. 이것의 의미는, 문자열의 일부만을 변경할 수 없다는 말이다.
var hello string = "Hello World"
hello = "How r u" // OK
hello[2] = 'a' // ERROR
바꿀 수 있는 방법은, 문자열을 슬라이스 배열로 새로 할당하여, (타입 변환하여)
배열의 요소를 바꾸는 방법이 있다.
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)
}
📌 문자열 합산
문자열을 합산한다?
→ 실제로 합쳐지는 것이 아니라, 둘을 더한 문자열을 새로운 주소에 할당하는 것이라고 이해하면 쉽다.
문자열 불변 원칙을 준수하는 것이다.
그러므로 string 합 연산을 빈번하게 하면 메모리가 낭비된다.
이에 대한 해결책이 strings 패키지의 Builder를 이용하는 방법이다.
Builder 사용 예제
문자열의 소문자를 모두 대문자로 바꾸는 예제이다.
- ToUpper1() 함수는 일반적인 합 연산을 구현했고,
- ToUpper2() 함수는 Builder를 이용하여 메모리 연산을 줄였다.
package main
import (
"fmt"
"strings"
)
func ToUpper1(str string) string {
var rst string
for _, c := range str {
if c >= 'a' && c <= 'z' {
rst += string('A' + (c - 'a')) // 합 연산 사용
} else {
rst += string(c)
}
}
return rst
}
func ToUpper2(str string) string {
var builder strings.Builder
for _, c := range str {
if c >= 'a' && c <= 'z' {
builder.WriteRune('A' + (c - 'a')) // strings.Builder 사용
} else {
builder.WriteRune(c)
}
}
return builder.String()
}
func main() {
var str string = "Hello World"
fmt.Println(ToUpper1(str))
fmt.Println(ToUpper2(str))
}
👀 왜 문자열은 불변 원칙을 지키려 할까?
예기치 못한 버그를 방지하기 위해서.
'3-1기 스터디 > Golang' 카테고리의 다른 글
[9주차] 고루틴과 동시성 프로그래밍, 채널과 컨텍스트 (0) | 2022.01.20 |
---|---|
[6주차] 숫자 맞추기 게임 만들기, Go 언어의 슬라이스 (0) | 2021.12.01 |
[4주차] Go 언어의 배열, 구조체, 포인터 (0) | 2021.11.22 |
[3주차] Go언어의 if, switch, for (0) | 2021.11.11 |
[2주차] Go 언어의 연산자, 함수, 상수 (0) | 2021.11.03 |
댓글