[Go] 09. 문자열

2024. 1. 18. 17:00Go

반응형

문자 집합으로 타입명은 string입니다.
쌍따옴표(") 또는 backtick(`)으로 감싸서 정의합니다.

backtick으로 감쌀 경우, 문자열 안에 들어있는 특수문자(\t, \n 등)를 문자자체로 인식합니다.

  • utf-8을 표준 문자코드로 사용한다
    • ansi에 있는 문자들은 1byte로 표현(영문자, 숫자, 일부특수문자)
      • ansi = ascii코드(7bit)의 확장판
    • 그외의 문자들은 2~3byte로 표현
    • ansi문자에 해당하는 문자는 1byte로 표현하면서 다국어문자를 표현가능하기 때문에 합리적입니다
  • 문자열 일치여부는 ==, != 로 비교합니다
  • >, >=, <, <=로 문자열 비교할 수 있고, 문자열 비교는 맨 앞글자부터 한 글자씩 비교합니다.
  • 문자열 대입시 문자열 자체의 복사가 아니라 같은 메모리를 가리키도록 하기 때문에, 문자열 대입은 성능에 영향을 주지 않습니다.
    • 문자열의 주소값까지 함께 복사됩니다.
  • 문자열은 불변 입니다

쌍따옴표로 감싼 문자열은 특수문자 처리가 되지만

s1 := "apple\tbanana\nmelon"  
fmt.Println(s1)  
apple   banana
melon

backtick으로 감싼 문자열은 문자로 인식합니다.

s2 := `red\tblue\norange`  
fmt.Println(s2)
red\tblue\norange

1. rune

이전에 변수에 대한 포스팅에서 잠깐 소개했었던 rune 데이터 타입은 int32 데이터 타입의 alias입니다.
utf-8문자를 표현하기 위해서는 최소 1byte ~ 3byte가 필요합니다.

그러나, 3byte에 해당하는 정수타입은 없기 때문에 4byte 정수타입인 int32타입을 문자 하나를 표현하는데에 사용합니다.

문자하나를 표현되는 int32타입을 편의상 rune 이라는 타입을 이용하여 정의합니다.

java 에서 char값을 숫자와 문자로 표현할 수 있었던 것 처럼, go에서도 rune타입의 값을 문자나 숫자로 표현할 수 있습니다

var ch rune  
ch = '가'  
fmt.Printf("%%c : %c, %%d: %d, 데이터 타입: %T", ch, ch, ch)
%c : 가, %d: 44032, 데이터 타입: int32

2. 문자열 메모리크기

len() 내장함수를 문자열에 사용하면, 그 문자열의 메모리 크기를 구할 수 있습니다. (문자열 길이가 아닙니다)

ansi문자에 해당하는 영문자, 숫자는 각 문자당 1byte의 메모리 크기를 차지하기 때문에 4가 출력되지만

s1 := "abC4"  
fmt.Println(len(s1))
4

한글은 한문자당 3byte의 메모리 크기를 차지하기 때문에 3 * 3 = 9 가 출력됩니다.

s2 := "가나다"   
fmt.Println(len(s2))
9

3. 문자열 길이

문자열에 포함된 글자수(= 길이)를 알아내고 싶다면, 문자열을 []rune 타입으로 변환한 후 len() 내장함수로 길이를 구하면 됩니다.

[]rune 은 가변길이 배열인 slice 타입입니다.

s1 := "abC4"  
s2 := "가나다"  

r1 := []rune(s1)  
r2 := []rune(s2)  

fmt.Println(len(r1))  
fmt.Println(len(r2))
4
3

[]rune 타입으로 변경한 후에는 문자열 길이를 구할 수 있습니다.


4. 문자열 순회

4.1. 잘못 작성한 문자열 순회

문자열에 len() 함수를 적용할 경우, 문자열 메모리 크기를 반환합니다.

그렇기 때문에 java에서 자주 이용하는 방식대로 for i 반복문을 이용할 경우,
문자하나씩 출력되는 것이 아니라, 1byte씩 읽어들이기 때문에
ANSI문자에 해당하는 A, B, 1, 2는 잘 출력되지만 가, 나 는 각각 3개씩 쪼개져서 이상한 문자로 출력되게 됩니다.

또, 이 문자들의 데이터타입은 uint8로 출력되는데
uint8은 alias로 byte와 같은 타입입니다.
문자열에 len을 바로 적용할 경우 byte단위로 읽어집니다.

s := "AB12가나"  
for i := 0; i < len(s); i++ {  
  fmt.Printf("%%d: %d, %%c: %c (%T)\n", s[i], s[i], s[i])  
}
%d: 65, %c: A (uint8)
%d: 66, %c: B (uint8)
%d: 49, %c: 1 (uint8)
%d: 50, %c: 2 (uint8)
%d: 234, %c: ê (uint8)
%d: 176, %c: ° (uint8)
%d: 128, %c: � (uint8)
%d: 235, %c: ë (uint8)
%d: 130, %c: � (uint8)
%d: 152, %c: � (uint8)

4.2. []rune 변환후 순회

문자열을 []rune 타입으로 변환하는 것은 문자로 이뤄진 가변배열로 변환하는 것이기 때문에,
배열의 각 요소를 출력하면 문자를 순회하는 것이 가능합니다.

s := "AB12가나"
arr := []rune(s)  
for i := 0; i < len(arr); i++ {  
  fmt.Printf("%%d: %d, %%c: %c (%T)\n", arr[i], arr[i], arr[i])  
}
%d: 65, %c: A (int32)
%d: 66, %c: B (int32)
%d: 49, %c: 1 (int32)
%d: 50, %c: 2 (int32)
%d: 44032, %c: 가 (int32)
%d: 45208, %c: 나 (int32)

4.3. range 키워드로 순회 추천**

더 간편한 방식으로, for range를 string에 적용하면, 문자열을 순회할 수 있습니다.
[]rune 타입으로 변환하는 방법에서는, 데이터타입 변환을 위한 작업(메모리 할당 등...)을 행해야하지만, for range로 문자열을 순회하는 방법은 그러한 작업이 없기 때문에 메모리 낭비

s := "AB12가나"
for _, v := range s {  
  fmt.Printf("%%d: %d, %%c: %c (%T)\n", v, v, v)  
}
%d: 65, %c: A (int32)
%d: 66, %c: B (int32)
%d: 49, %c: 1 (int32)
%d: 50, %c: 2 (int32)
%d: 44032, %c: 가 (int32)
%d: 45208, %c: 나 (int32)

5. 문자열 합치기

+ 연산자를 이용하여 문자열을 합칠 수 있으며,
+= 를 이용하여 기존 문자열에 문자열을 합칠 수도 있습니다.

s1 := "hi"  
s2 := "wow"  
s3 := s1 + " " + s2  
s1 += " " + s2  

fmt.Println(s3)  
fmt.Println(s1)
hi wow
hi wow

6. 문자열 대입

문자열을 대입할 때에는, 다른 변수들과 달리 주소값도 함께 복사됩니다.
따라서, 새로 복사되는 것이 아니라 같은 메모리를 바라보게 되기 때문에 성능을 떨어뜨리지 않습니다.

문자열은 내부에 포인터와 길이 필드로 구성되어있습니다. 아래 예제를 통해 실제 문자열이 가리키고 있는 메모리의 주소와 길이 등을 확인해볼 수 있습니다.

package main  

import (  
  "fmt"  
  "reflect"  "unsafe")  

func main() {  
  s1 := "apple"  
  s2 := s1  
  fmt.Printf("s1의 주소: %p, %d\n", unsafe.StringData(s1), unsafe.StringData(s1))  
  fmt.Printf("s2의 주소: %p, %d\n", unsafe.StringData(s2), unsafe.StringData(s2))  

  fmt.Println((*reflect.StringHeader)(unsafe.Pointer(&s1)))  
  fmt.Println((*reflect.StringHeader)(unsafe.Pointer(&s2)))  
}
s1의 주소: 0x100f4ca75, 4311009909
s2의 주소: 0x100f4ca75, 4311009909
&{4311009909 5}
&{4311009909 5}

StringHeader 구조체는 주소와 문자열길이를 담고 있습니다.


7. 문자열은 불변

문자열은 불변타입입니다.

이러한 성질로 인하여 문자열 내의 특정 문자를 변경하는 것은 불가하며
문자열 합성시에는 새로운 메모리공간에 두 문자열을 합치는 행위를 합니다.

아래의 예제코드에서 문자열 합산 이후 s1의 주소가 변경된 것을 확인할 수 있습니다.

package main  

import (  
  "fmt"  
  "unsafe")  

func main() {  
  s1 := "test"  
  fmt.Printf("최초의 s1의 주소: %p\n", unsafe.StringData(s1))  

  s1 += "123"  
  fmt.Printf("문자열 합산 후 s1의 주소: %p\n", unsafe.StringData(s1))  
}
최초의 s1의 주소: 0x102428650
문자열 합산 후 s1의 주소: 0x140000a4012

8. 문자열 합산이 빈번하다면 strings.Builder로

문자열 합신이 빈번하다면, strings 패키지의 Builder를 이용하면 메모리 낭비를 줄일 수 있습니다.

strings.Builder는 내부에 []byte 를 가지고 있기 떄문에 문자 추가시 매번 메모리를 새로 생성하지 않기 때문에 공간 낭비가 적습니다.

package main  

import (  
  "fmt"  
  "strings")  

func main() {  
  fmt.Println(ToUpper("Hello World!"))  
}  

func ToUpper(s string) string {  
  var builder strings.Builder  
  for _, ch := range s {  
   if ch >= 'a' && ch <= 'z' {  
    builder.WriteRune('A' + (ch - 'a'))  
   } else {  
    builder.WriteRune(ch)  
   }  

  }  
  return builder.String()  
}
728x90
반응형

'Go' 카테고리의 다른 글

[Go] 12. 메서드  (0) 2024.01.21
[Go] 10. 패키지  (0) 2024.01.20
[Go] 08. 포인터  (0) 2024.01.18
[Go] 07. 배열, 구조체  (1) 2024.01.17
[Go] 06. if문, switch문, for문  (0) 2024.01.16