본문 바로가기
Language/Golang

Golang (Go언어) 문자열(String)

by HeeWorld 2024. 11. 8.

Tucker의 Go 언어 프로그래밍 책과 유튜브를 통해 학습 중입니다.

 

Golang 마스코트 Gopher(고퍼)

 

문자열

- 문자 집합으로, 타입명은 string

- 문자열은 큰따옴표(")나 백쿼트(`)로 묶어서 표시(그레이브(grave)라고도 부름)

- 백쿼트로 문자열을 묶으면 문자열 안의 특수 문자가 일반 문자처럼 처리 됨

package main

import "fmt"

func main() {

	poet1 := "죽는 날까지 하늘을 우러러\n 한 점 부끄럼이 없기를,\n잎새에 이는 바람에도\n나는 괴로워했다.\n"

	poet2 := `죽는 날까지 하늘을 우러러
한 점 부끄럼이 없기를,
잎새에 이는 바람에도 
나는 괴로워했다.`

	fmt.Println(poet1)
	fmt.Println(poet2)
}

// 결과

죽는 날까지 하늘을 우러러
 한 점 부끄럼이 없기를,
잎새에 이는 바람에도
나는 괴로워했다.

죽는 날까지 하늘을 우러러
한 점 부끄럼이 없기를,
잎새에 이는 바람에도 
나는 괴로워했다.

 

✓ 백쿼트로 묶을 경우 여러 줄에 걸쳐서 문자열을 쓸 수 있지만, 큰따옴표는 한 줄만 묶을 수 있음

✓ 큰따옴표로 문자열을 묶을 때 특수 문자를 그대로 사용하고 싶다면, 역슬래시(\)를 두 번 적어주면 됨

    ex) "Hello\\tWorld\\n" = 출력 "Hello\tWorld\n"

 

 

UTF-8 문자 코드

- Go는 UTF-8 문자코드를 표준 문자코드로 사용

- 문자 한 개를 1~4byte로 표현하는데, 보통 1~3byte로 표현됨 (한 문자가 최대 4byte)

- UTF-8은 첫 비트(시작)를 보는데, (1byte는 8bit) 첫 비트가 0으로 시작하면 1byte로 인식

  첫 비트가 1로 시작하면, 1byte가 아닌 것으로 생각하고 두 번째 비트를 확인하여 1이 몇 개 오는지 확인(ex 1 1 0 ~ = 2byte, 1 1 1 0 ~ = 3byte)

- UTF-8은 자주 사용되는 영문자, 숫자, 일부 특수 문자를 1바이트로 표현하고, 그외 다른 문자들은 2~3byte로 표현

 

✓rune 타입으로 한 문자 담기

- 문자 하나를 표현하는데 'rune' 타입을 사용

- UTF-8은 한 글자가 1~3 바이트 크기이기 때문에 UTF-8 문자값을 가지려면 3byte가 필요하나,

   Go 언어 기본 타입에서 3byte 정수 타입은 제공하지 않음 → rune 타입은 4byte 정수 타입인 int32 타입의 별칭 타입

package main

import "fmt"

func main() {

	var char rune = '한'

	fmt.Printf("%T\n", char)
	fmt.Println(char)
	fmt.Printf("%c\n", char)
}

// 결과

int32
54620
한

 

fmt.Printf("%T\n", char)

- %T를 사용하여 char 변수의 타입을 출력하는데, rune 타입은 int32타입과 같기 때문에 int32가 출력됨

 

fmt.Println(char)

- char 타입이 int32라서 숫차로 출력

 

fmt.Printf("%c\n", char)

- %c 포맷을 사용해서 문자 하나를 출력

 

 

✓ 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

 

fmt.Printf("len(str1) = %d\n", len(str1))

- 한글 문자열 크기는 15로, UTF-8에서 한글은 글자당 3byte를 차지하기 때문에 총 3 * 5 = 15라서 15byte가 나옴


fmt.Printf("len(str2) = %d\n", len(str2))

- 영문 문자열 크기는 5로, UTF-8에서 영문자는 글자당 1byte라서 총 5byte가 나옴

 

 

✓ []rune 타입 변환으로 글자수 알아내기

- string 타입, rune 슬라이스 타입인 []rune타입은 상호 타입 변환이 가능 (슬라이스는 길이가 변할 수 있는 배열)

package main

import "fmt"

func main() {

	str := "Hello World"

	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"는 문자들의 집합이고, 각 문자들은 UTF-8 코드 0x48, 0x65 등의 값을 갖음

   = 문자열은 문자코드처럼 각 문자의 코드 값의 배열인 rune배열로 나타낼 수 있음

- string 타입과 []rune 타입은 모두 문자들의 집합을 나타내기 때문에 상호 타입 변환이 가능

  = 타입 변환을 할 경우 rune 배열의 각 요소에는 문자열의 각 글자가 대입됨

 

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

 

- 한글은 문자당 3byte를 차지하고 영문은 1byte를 차지하여 "Hello 월드"는 총 12byte가 됨

- len(str)과 같이 string 타입 변수의 길이는 문자열의 바이트 길이가 반환

- string 타입을 []rune으로 타입 변환을 하면 각 글자들로 이뤄진 배열로 반환

- 따라서, len(runes)와 같이 len()의 인수로 배열을 넣으면 배열의 요소 개수를 반환함.

 

→ string 타입은 연속된 byte메모리 라면, []rune 타입은 글자들의 배열로 이뤄져 있음

    둘은 완전 다른 타입이지만, 편의를 위해 Go언어는 둘의 상호 타입 변환을 지원함.

 

 

문자열 순회

1. 인덱스를 사용한 바이트 단위 순회

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 문자값:œ

 

- Hello가 출력되고, 타입은 uint8(1byte), 값은 72, 문자값은 H ... 등으로 출력됨 (대문자가 소문자보다 값이 작음)

- 빈칸도 문자로 인식함

- 한글은 ASCII 코드 범위 밖에 있어 3byte로 표현해야 하는데, 각 1byte 별로 출력해서 한글 문자값이 출력되지 않음

 

2. []rune으로 타입 변환 후 한글자씩 순회

package main

import "fmt"

func main() {

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

	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 문자값:드

 

- 문자열을 rune을 사용하여 int32타입으로 타입 변환

- rune은 한 칸이 4byte로 "Hello 월드"는 4 * 8 = 32byte를 차지하게 됨

- rune 배열로 선언을 하면, 한 글자당 하나씩(한 칸을 차지) 바꿀 수 있음 

 

3. range 키워드를 이용해 한 글자씩 순회

package main

import "fmt"

func main() {

	str := "Hello 월드"
	for _, v := range str {
		fmt.Printf("타입:%T 값:%d 문자값:%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 문자값:드

 

- range를 사용하면 추가 메모리 할당 없이 문자열을 한 글자씩 순회할 수 있어서 불필요한 메모리 낭비를 없앨 수 있음

 

 

문자열 합산

- 문자열은 +과 += 연산을 사용해서 문자열을 이을 수 있음

package main

import "fmt"

func main() {

	str1 := "Hello"
	str2 := "world"

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

	str1 += " " + str2
	fmt.Println(str1)
}

// 결과

Hello world
Hello world

 

str3 := str1 + " " + str2

- str1, " "(빈칸), str2를 이음

 

str1 += " " + str2

- str1에 " "(빈칸) + str2 문자열을 붙임

 

✓ Go에서는 문자열 연산으로 + 연산자만 제공되고, -나 * 연산자는 제공되지 않음

 

 

문자열 비교하기

- 연산자 ==, !=를 사용해서 문자열이 같은지 같지 않은지 비교

- 두 문자열이 완전히 같을 때 == 연산 결과가 true가 되고, != 연산은 false가 됨

- 두 문자열이 한 글자라도 다르거나 길이가 다르면 != 연산 결과가 true가 되고, == 연산은 false가 됨

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)
}

// 결과

Hello == Hell : false
Hello != Hell : true
Hello == Hello : true
Hello != Hello : false

 

문자열 대소비교: >, <, <=, >=

- 문자열 대소비교는 첫 글자부터 하나씩(같은 위치에 있는 글자끼리) 값을 비교해서 그 글자에 해당하는 유니코드 값이

   다를 경우 대소를 반환  = ex) "Hello" < "World"를 비교 했을 때, l에서 같은 값이 나오기 때문에(<) 이는 false

- 대문자가 더 작다 = 'A'-'Z': 65~90, 'a'-'z': 97~122 

 

 

문자열 구조

- string은 문자 공간을 가리키는 것이 아니고, 해당 문자가 있는 공간의 주솟값을 가지고 있음(=포인터 변수)

- 메모리 주소는 항상 고정되어있음 = size (64bit은 8byte, 32bit는 4byte)

- 문자 공간의 길이가 늘어나던 줄어들던 string이 가지고 있는 값의 사이즈는 8byte로 고정되어 있음

type StringHeader struct {
    Data uintptr
    Len  int
}

 

Data uintptr

- uintptr = uint pointer(row pointer)의 약자로, Go에서 기본적으로 사용할 수 없지만 내부에서는 사용할 수 있음

   = 메모리 주소를 가지고 있는 포인터 (8byte)

   → 메모리 어딘가에 실제 문자열이 들어있는 배열, 그 배열의 주솟값을 str Data가 가리키고 있음

 

Len int

- 문자 길이(byte 개수)를 가지고 있음(int타입으로 문자열의 길이를 나타냄)

string 구조

var str1 string = "Hello"
var str2 string

str2 = str1

 

var str 1 string = "Hello"

- "Hello"는 메모리 어딘가에 저장이 되어있고(6byte), 주소지가 100번이라고 가정한 뒤 str1은 2개의 필드(field)를 가지고 있음

    첫 번째가 Data, 두 번째가 길이(len)인데 data는 "Hello"의 주소 값인 100, len은 "Hello"의 byte 길이인 6을 가지고 있음

 

var str2 string

- 동일하게 두 개의 필드를 가지고 있고, 두 개의 값은 기본값으로 nil이고(빈 문자열), str1와 str2의 사이즈가 같음

   str1의 값을 str2에 복사하는 것이기 때문에 str2도 data에 100, len에 6이 복사되어 두 개의 변수는 같은 공간을 가리키고 있게 됨

 

str2 = str1

- str2도 Hello가 됨(같은 문자열을 가리키고 있음)

string 구조2

 

문자열 대입

- 전체 문자열이 100byte가 초과하여도, 복사하는 양은 16byte(Data 8byte, Len 8byte)만 복사가 됨

package main

import "fmt"

func main() {

	str1 := "안녕하세요. 한글 문자열입니다."
	str2 := str1

	fmt.Printf(str1)
	fmt.Printf("\n")
	fmt.Printf(str2)
}

// 결과

안녕하세요. 한글 문자열입니다.
안녕하세요. 한글 문자열입니다.

 

- 구조체 변수가 복사될 때 구조체 크기만큼 메모리가 복사됨

- str1과 str2는 모두 구조체이므로 각 필드, 즉 Data 포인터값과 Len값이 복사 됨.

package main

import (
	"fmt"
	"unsafe"
)

type StringHeader struct {
	Data uintptr
	Len  int
}

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

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

	fmt.Println(stringHeader1)
	fmt.Println(stringHeader2)
}

// 결과

&{4915151 12}
&{4915151 12}

 

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

- (&str1)은 str1의 메모리 주소값을 unsafe패키지에 있는 Pointer 타입으로 타입 변환한 것을 StringHeader로 다시 변환

- StringHeader는 Data와 Len형태로 변환

- 결과 중 첫 번째는 Data 값 즉, "Hello World!"의 메모리 주소값, 두 번째는 Len으로 12byte를 출력


✓ stringHeader2의 결과도 위와 같으며, stringHeader1과 2는 같은 곳을 바라보고 있음!

 

 

문자열은 불변(immutable)

- 불변이라는 말은 string 타입이 가리키는 문자열의 일부만 변경할 수 없다는 것

   = 문자열 전체 "Hello"에서 "World"로 변경은 가능하지만, l을 a로 변경 불가능

var str string = "Hello World"
str = "How are you?"   // 전체 변경 가능
str[2] = 'a'   // Error! 일부 변경 불가능

 

package main

import "fmt"

func main() {
	var str string = "Hello World"
	var slice []byte = []byte(str)

	slice[2] = 'a'

	fmt.Println(str)
	fmt.Printf("%s\n", slice)
}

// 결과

Hello World
Healo World

 

기존의 string은 그대로 "Hello World"를 가지고 있고, slice는 3번째를 a로 바꿔서 출력함

→ 기존의 값이 바뀌지 않았다는 것은 두 개는 서로 다른 공간이라는 것

 

 

문자열 합산

- Go언어에서 string 타입 간 합 연산을 지원

- 합 연산을 하면 두 문자열이 하나로 합쳐지게 됨

- 기존 문자열 메모리 공간을 건드리지 않고, 새로운 메모리 공간을 만들어서 두 문자열을 합치기 때문에 string 합 연산 이후 주솟값이 변경됨

   = 문자열 불변 원칙 준수

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	var str string = "Hello"
	addr1 := unsafe.StringData(str)
	str += " World"
	addr2 := unsafe.StringData(str)
	str += " Welcome!"
	addr3 := unsafe.StringData(str)

	fmt.Println(str)
	fmt.Printf("addr1:\t%p\n", addr1)
	fmt.Printf("addr2:\t%p\n", addr2)
	fmt.Printf("addr3:\t%p\n", addr3)
}

// 결과

Hello World Welcome!
addr1:	0x4b00e8
addr2:	0xc000112040
addr3:	0xc000128000

 

✓ string 합 연산을 빈번하게 하면 메모리가 낭비되며, string 합 연산을 빈번하게 사용하는 경우

    strings 패키지의 Builder를 이용해서 메모리 낭비를 줄일 수 있음.

 

package main

import (
	"fmt"
	"strings"
)

func ToUpper1(str string) string {
	var rst string
	for _, c := range str {
		if c >= 'a' && c <= 'z' { //c가 d라는 가정하에, a가 97이고 z가 122라 97~122 사이 값을 의미
			rst += string('A' + (c - 'a')) //대문자 A는 65이며, 65 + (100 - 97)= 68, 68은 대문자 D로 a에서 3떨어져있음
		} 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'))
		} else {
			builder.WriteRune(c)
		}
	}
	return builder.String()
}

func main() {
	var str string = "Hello World"

	fmt.Println(ToUpper1(str))
	fmt.Println(ToUpper2(str))
}

// 결과

HELLO WORLD
HELLO WORLD

 

- Go 언어 내부에서는 합 연산을 사용할 때마다 새로운 메모리 공간을 할당해서 두 문자열을 더함.

   = 즉, 합 연산을 할 때마다 메모리 공간이 버려지고, 메모리 공간 방비와 성능 문제가 발생됨.

 

- strings.Builder는 내부에 슬라이스를 가지고 있기 때문에 WriteRune() 메서드를 통해 문자를 더할 때, 매번 새로운 메모리를 생성하지 않고

   기존 메모리 공간에 빈자리가 있으면 더하게 됨. = 메모리 공간 낭비 없음

'Language > Golang' 카테고리의 다른 글

Golang (Go언어) 패키지(Package)  (6) 2024.11.09
Golang (Go언어) 포인터(Pointer)  (3) 2024.11.07
Golang (Go언어) 구조체(Structure)  (9) 2024.11.05
Golang (Go언어) 배열(Array)  (0) 2024.11.02
Golang (Go언어) for문 (반복문)  (4) 2024.10.31