문자열

문자의 집합(배열)이라는 의미이다.

기존의 문자는 ASCII코드로 1byte를 갖기 때문에 0255로 총 255개의 문자를 표현할 수 있었는데 이 수로는 현재 존재하는 언어(문자)들을 모두 표시할 수 없기 때문에 더 큰 byte의 문자가 필요해졌다. 대표적으로 UTF-8이 있는데 이는 한 문자당 14byte를 갖고 UTF-162byte를 갖는다.


1. UTF-8과 UTF-16

UTF-16이 공간을 더 적게 차지하기 때문에 UTF-16이 더 좋아보일 수 있다. 하지만, UTF-8은 파레토의 법칙으로 데이터를 잘 봐보니 영어/숫자의 데이터가 대부분(80%)를 차지하더라 라는 개념으로 필요한 데이터크기에 맞게 크기를 할당하는 개념으로 크기가 동적으로 변한다.

UTF-16은 2byte로 고정길이기 때문에 UTF-8이 더 효율적일 수도 있고 ASCII로 표현되는 데이터들을 2byte로 표현을 하다보니까 ASCII와 상호간에 호환이 잘 안되는 문제점이 있다.



2. 문자열 리터럴 표현 방식

1) Double Quote ("")

str := "\""
fmt.Println(str)      //"

str1 := "Hello\nWorld"
fmt.Println(str1)     //Hello
                      //World

str2 := "Hello
World"    //error

"" 은 한 줄로 표현할 때 사용하며 개행을 하기 위해선 개행문자 \n 을 이용해서 해야하고 예제처럼 한줄 띄어서 작성할 수 없다.

Go에서 쌍따옴표로 열리고 닫힌 string을 Interprected string이라고 한다. 이 문자열은 escape 문자()를 이용해서 특수기호를 표시할 수 있다.


2) Back Tick (``)

str := `"`
fmt.Println(str)      //"

str1 := `Hello\nWorld`
fmt.Println(str1)     //Hello\nWorld

str2 := `Hello
World`
fmt.Println(str2)     //Hello
                      //World

여러줄로 표현 가능하고 이를 이용한 string을 Raw string이라고 하고 이는 내부 특수기호를 그대로 표현해준다.



3. 문자열 순회

str := "Hello 월드"

ftm.Println(len(str))     //case 1

for i:=0; i < len(str); i++ {
  fmt.Printf("타입: %T, 값: %d, 문자값: %c\n",str[i],str[i],str[i])
}
fmt.Printf("%c",str[8])   //”

위에서 case1의 출력결과는 무엇일까?

우리 예상은 8일 것 같지만 len()은 문자열의 바이트 길이를 반환하기 때문에 한글은 3byte씩 해서 총 12가 출력된다. 그렇기 때문에 위의 for문을 실행해보면 한글이 나오는 부분부터 깨지는 것을 볼 수 있다.

[]를 이용해 문자열을 접근하는 것은 len과 같이 메모리의 index에 접근하는 것이기 때문에 3byte로 저장된 한글 한문자중에서 1byte만 접근해 쓰레기값이 나오는 것이다.


1) []rune

str := "Hello 월드"
arr := []rune(str)

for i:=0; i < len(arr); i++ {
  fmt.Printf("타입: %T, 값: %d, 문자값: %c\n",arr[i],arr[i],arr[i])
}

ftm.Println(len(arr))     //8

rune은 int32의 별칭타입이기 때문에 4byte를 갖는다. 이를 이용해서 문자열을 rune타입 슬라이스로 만들어서 배열을 순회하면 우리가 원하는데로 동작한다.


2) range

str := "Hello 월드"

for _,v := range str {
  fmt.Printf("타입: %T, 값: %d, 문자값: %c\n",v,v,v)
}

정확한 길이가 필요한 것이 아니라 단순히 순회가 목적이라면 range를 이용하면 편하게 순회 할 수 있다.



4. 문자열 합산

str1 := "Hello"
str2 := "World"

str3 := str1 + " " + str2
fmt.Println(str3)

str3 += "Hi "
fmt.Println(str3)

go는 +를 통해 문자열 합을 지원하고 ptyhon같이 그 외의 연산(*…)을 지원하지 않는다.



5. 문자열 비교

1) ==, !=

str1 := "Hello 월드"
str2 := "Hello 월드"
fmt.Println(str1 == str2)   //true

str1 := "Hello 월드\n"
str2 := `Hello 월드
`
fmt.Println(str1 == str2)   //true

문자열 두개가 서로 같은지 비교하는 연산을 지원한다.

2) <, > , <= ,>=

str1 := "apple"
str2 := "Apple"

fmt.Println(str1 > str2) //true

go 는 대소 비교도 지원하는데 이때 사전식 비교로 대문자가 더 작다.

Ascii 코드 값

  • A-Z : 65 - 90

  • a-z : 97 - 122



6. 문자열 구조

지금까지 Go를 착실히 공부했다면 여기서 한가지 의문점이 들 것이다.

분명히 Go는 강타입 언어라서 연산,대입을 할때 데이터타입이 크기까지 똑같아야 했다. int8과 int32 두 타입의 연산을 시도하면 error가 발생한다. 그런데 string은 데이터크기가 다 제각각인데 어떻게 합이나 비교, 대입이 가능할까?


type StringHeader struct{
  Data uintptr
  Len int
}

문자열은 내부적으로 데이터(문자열 리터럴)의 주소값을 포인터변수로 가지고 있기 때문에 string의 길이가 서로 달라도 연산이 가능하고 대입연산도 가능하다.


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

func main(){
  str1 := "Hello 월드"
  str2 := str1

  stringHeader1 := (*reflect.StringHeader)(unsafe.Pointer(&str1))
  stringHeader2 := (*reflect.StringHeader)(unsafe.Pointer(&str2))

  fmt.Println(stringHeader1)    //&{49633883 12}
  fmt.Println(stringHeader2)    //&{49633883 12}
}

문자열을 poninter타입으로 변환후 reflect를 이용해 내부를 봐보면, 실제 문자열이 저장된 위치(49633883)와 길이(12)가 들어있는 것을 볼 수 있다.


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

func main(){
  str1 := "Hello"
  str2 := "Hello"

  stringHeader1 := (*reflect.StringHeader)(unsafe.Pointer(&str1))
  stringHeader2 := (*reflect.StringHeader)(unsafe.Pointer(&str2))

  fmt.Println(stringHeader1)    //&{4989886 5}
  fmt.Println(stringHeader2)    //&{4989886 5}
}

str2에 str1을 할당하는 것이 아니라 같은 문자 리터럴을 대입해도 같은 공간을 가리키는 것을 볼 수 있는데, 이는 메모리(스택이나 힙) 또는 코드공간에 해당 문자열이 있는지 확인하고 있다면 해당 공간의 주소를 대입시켜주기 때문이다.



7. 문자열 불변

str1 := "Hello World"
slice := []byte(str)

str1[2] = 'a' //error
slice[2] = 'a'

fmt.Println(str1)         //Hello World
fmt.Printf("%s\n",slice)  //Healo Wolrd

문자열은 Immutable한 속성을 가지고 있어 문자리터럴값은 내부적으로 변경이 불가능하다. 그래서 문자열을 []로 접근해서 일부만 수정하려고 하면 error가 발생한다.

또한, Slice로 문자열을 복사하게 되면 두개의 문자열은 서로 다른 메모리(공간)으로 복사를 하기 때문에 slice를 수정을 해도 기존의 문자열은 변경이 되지 않는다.


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

func main(){
  str1 := "Hello "
  str2 := str1

  stringHeader1 := (*reflect.StringHeader)(unsafe.Pointer(&str1))
  stringHeader2 := (*reflect.StringHeader)(unsafe.Pointer(&str2))

  fmt.Println(stringHeader1)    //&{4990223 6}
  fmt.Println(stringHeader2)    //&{4990223 6}

  str2 += "World"

  fmt.Println(stringHeader1)    //&{4990223 6}
  fmt.Println(stringHeader2)    //&{824634237040 11}
}

두개의 문자열이 처음에는 같은 메모리를 가리키고 있다가 +연산을 수행하게 되면 기존의 문자열을 바꾸는 것이 아니라 새로운 공간에 합한 문자열을 할당하는 것을 볼 수 있다.



8. String Builder

import (
  "strings"
  "fmt"
)

func main(){
  str := "hello world"
  builder := strings.Builder{}

  for _,c := range str {
    if c >= 97 && c <= 122{
      builder.WriteRune('A' + (c - 'a'))
    }else {
      builder.WriteRune(c)
    }
  }
  fmt.Println(builder.String())     //HELLO WORLD
  fmt.Println(strings.ToUpper(str)) //HELLO WORLD
}

Java의 StringBuilder와 동일한 역할의 string builder이다.

위에서 본 것처럼 문자열 합산연산시 +은 계속 새로운 공간을 할당해서 연산이 수행되는데 많은 문자열을 처리하는데 있어 이는 매우 비효율적이다. 계속 공간을 할당하고 GC가 지우고를 반복하기 때문이다.

그런데 strings.Builder를 이용하면 공간할당을 새로하지 않으면서 붙일 수 있다. Builder 구조체 내부에 byte슬라이스([]byte)를 이용해서 문자열을 관리하기 때문에 +연산시 새로 공간이 할당되는 문제를 해결할 수 있다.





Reference

『Tucker의 Go 언어 프로그래밍』 스터디 요약 노트

Tucker의 Go 강좌

Tags :

Related Posts

[WSL2] 포트포워딩과 window에서 workbench로 접속하기

[WSL2] 포트포워딩과 window에서 workbench로 접속하기

WSL2를 이용하여 개발을 진행서 외부에서 접근하고 싶거나, 배포를 위해 접근하고 싶을 수가 있는데, 문제가 되는 것이 WSL2는 VM과 같은 환경이라 별도의 IP를 갖는다는 점이다. 그러면 포트포워딩을 하면 되지 않느냐라고 할 수 있는데 맞다 포트포워딩을 하면된다 하지만 재부팅을 할때마다 변경되는 IP에 매번 포트 포워딩을 할 수 없는 노릇이기에 Powershell 파일을 이용하여 재부팅마다 wsl2의 ip를 잡아 특정 포트를 포트포워딩 하는 방법을 남기려고 한다....

Read More
Spring boot와 EK스택 연결하기

Spring boot와 EK스택 연결하기

현재 진행하고있는 spring boot 프로젝트에 쿼리 통계를 위해 ElasticSearch와 Kibana를 도입하기로 했고 이에 맞게 로깅방법이나 세팅 방법을 기록하기 위해 글을 작성하게 됬다. diagram 구성은 다음과 같은데 서버로 EC2 프리티어를 사용하고 있어 사양이 안좋기 때문에 서버 내부에 logstash없이 filebeat를 이용하여 로그를 전송하기로 했다. ELK스택으로는 Elastic Cloud 프리티어를 이용해 빠르게 구축하는 것을 테스트해볼 생각이다....

Read More
Splay Tree

Splay Tree

Splaying이라는 기법이 사용되며, 이는 특정 노드에 대해 접근을 하면, 이를 루트로 위치하도록 재배치 하는 기법의 트리 1. 특징 구현이 단순 많이 접근한 노드에 대해서 빠른 접근이 가능하다 접근 빈도가 불균등하거나 비 랜덤 패턴의 경우 O(lgn)보다 더 유리하다. AVL-Tree와 RB-Tree와 달리 추가 데이터 저장 필요 없다. 최악의 경우 높이가 선형, 즉 O(n)이 나올 수 있다. 세그먼트 트리로 이용이 가능하다. k번째 원소 찾기, 구간 합, lazy Propagation, 구간 뒤집기 모두 쉽게 할 수 있다. x노드를 접근한다면, splay를 통해 root로 올라오면서 x보다 작은 노드들은 Left에, 큰 노드들은 Right에 모이는 특성을 이용...

Read More