15. 문자열

Winston Lee·2023년 11월 22일

Golang

목록 보기
9/12

1. 문자열 정의

  • 문자열은 큰따옴표나 백쿼트(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'
// 예제 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언어 창시자인 롭 파이크와 켄 톰슨이 고안한 문자코드이다.
    • 다국어 문자를 지원하고 문자열 크기를 절약할 목적으로 Go 언어의 창시자인 롭 파이크와 켄 톰슨이 고안한 문자코드
    • ANSI 문자들은 ANSI 순서 그대로 1바이트 사용, 그 외는 2 ~ 3 바이트로 1문자 표현
    • Go언어에서 1문자 표현은 rune 타입 사용 → 실제는 int32(4바이트)의 별칭
  • UTF-8의 특징
    - 다국어 문자를 지원
    - 자주 사용되는 영문자, 숫자, 일부 특수문자를 1 Byte로 표현하고 그 외 다른 문자들은 2~3 Byte로 표현한다.
    - UTF-16에 비해 크기를 절약할 수 있음.
    - ANSI 코드와 1:1 대응이 되어 ANSI로 바로 변환할 수 있다.

따라서 Go는 별다른 변환없이 한글이나 한자등을 사용할 수 있다.

  1. 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) // ❸ 문자 출력
}
  1. 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를 차지함.

  1. [ ]rune 타입 변환으로 글자 수 알아내기
    string 타입이자 rune 슬라이스 타입인 []rune 타입은 상호 타입 변환이 가능하다.

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 타입은 모두 문자들의 집합을 나타내므로 상호변환이 가능하다.
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 으로 타입 변환을 하면 각 글자들로 이루어진 배열로 변환된다.

2. 문자열 순회

  • string의 글자들을 하나씩 꺼내는 방법 → Go에서는 크게 3가지
    • 인덱스를 이용한 한 바이트씩 순회
    • []rune 타입으로 변환후 한 글자씩 순회
    • range를 이용한 한 글자씩 순회
package main

import "fmt"

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

	fmt.Println("----- 인덱스를 이용한 바이트 출력 -----")
	for i := 0; i < len(str); i++ {
		fmt.Printf(" 타입:%T 값:%d 문자값:%c\n", str[i], str[i], str[i])
	}

	fmt.Println("----- 인덱스를 이용한 문자([]rune 타입) 출력 -----")
	for i := 0; i < len(arr); i++ {
		fmt.Printf(" 타입:%T 값:%d 문자값:%c\n", arr[i], arr[i], arr[i])
	}

	fmt.Println("----- range 이용한 문자 출력 -----")
	for _, v := range str {
		fmt.Printf(" 타입:%T 값:%d 문자값:%c\n", v, v, v)
	}
}

----- 인덱스를 이용한 바이트 출력 -----
타입: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 문자값:œ
----- 인덱스를 이용한 문자([]rune 타입) 출력 -----
타입:int32 값:72 문자값:H
타입:int32 값:101 문자값:e
타입:int32 값:108 문자값:l
타입:int32 값:108 문자값:l
타입:int32 값:111 문자값:o
타입:int32 값:32 문자값:
타입:int32 값:50900 문자값:월
타입:int32 값:46300 문자값:드
----- range 이용한 문자 출력 -----
타입:int32 값:72 문자값:H
타입:int32 값:101 문자값:e
타입:int32 값:108 문자값:l
타입:int32 값:108 문자값:l
타입:int32 값:111 문자값:o
타입:int32 값:32 문자값:
타입:int32 값:50900 문자값:월
타입:int32 값:46300 문자값:드

3. 문자열 합치기

문자열은 +와 += 연산을 사용하여 문자열을 이을 수 있다.

//문자열 합치기
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)
}
  • 두 문자열이 같은지 다른지의 비교는 비교 연산자 == != 를 사용
  • 문자열의 대소비교도 역시 비교 연산자 > >= < <=를 사용
    • 문자열의 길이가 아니라 각 자리의 문자를 비교하여 대소를 비교한다
    • 틀린 문자가 있다면 해당 자리의 문자가 어느 것이 크고 작은 값을 가지느냐에 따라 비교
    • 길이는 len(string_var) , len([]rune_var)을 통해서 구한 값을 비교한다
//문자열 비교하기
var str1 string = "ABCDE"
var str2 string = "abcde"
if str1 > str2 {
	fmt.Println("str1 > str2")
} else if str1 < str2 {
	fmt.Println("str1 < str2")
}
// str1 < str2
//문자열 비교하기
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

4. 문자열 구조

5. 문자열은 불변(immutable)이다.

string 타입이 가리키는 문자열의 일부만 변경할 수 없다.

var str string = "Hello World"
str = "How are you?"  // ❶ 전체 바꾸기는 가능
str[2] = 'a'  // ❷ Error! 일부 바꾸기는 불가능

Go 언어는 슬라이스로 타입 변환을 할 때 문자열을 복사해서 새로운 메모리 공간을 만들어 slice 가 가리키도록 하여 불변 원칙을 지킴

// 문자열 일부 변경하기
var str string = "hello world"
var runeSlice []rune = []rune(str)
fmt.Println("Original string value :", str)
strHeader := (*reflect.StringHeader)(unsafe.Pointer(&str))
fmt.Println("Value of strHeader :", strHeader)
fmt.Printf("Pointer value of strHeader : %p\n\n", strHeader)
for i, v := range runeSlice {
	if v == 'o' {
		runeSlice[i] = '!'
	}
}
sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&runeSlice))
fmt.Println("Value of sliceHeader :", sliceHeader)
fmt.Println("Size of Slice :", unsafe.Sizeof(runeSlice))
fmt.Println()
str = string(runeSlice)
fmt.Println("Changed string value :", str)
fmt.Println("Value of strHeader :", strHeader)
fmt.Printf("Pointer value of strHeader : %p\n", strHeader)
/* OUTPUT
Original string value : hello world
Value of strHeader : &{4815093 11}
Pointer value of strHeader : 0xc000010240

Value of sliceHeader : &{824633811136 11 12}
Size of Slice : 24

Changed string value : hell! w!rld
Value of strHeader : &{824633819312 11}
Pointer value of strHeader : 0xc000010240
*/
package main

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

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

  // ❶ unsafe와 reflect 패키지를 이용해 내부 구조체로 변환
	stringheader := (*reflect.StringHeader)(unsafe.Pointer(&str))
	sliceheader := (*reflect.SliceHeader)(unsafe.Pointer(&slice))

  // ❷ 내부 구조체에서 가리키고 있는 메모리 주솟값을 16진수 형태로 출력
	fmt.Printf("str:\t%x\n", stringheader.Data) 
	fmt.Printf("slice:\t%x\n", sliceheader.Data)
}

// 서로 다른 주소를 가리키고 있음
str: 10243512b
slice: 14000108ec0

  • 문자열의 합산의 경우 메모리 위치
    • 기존의 문자열의 위치를 건드리지 않고, 새로운 문자열을 합 연산 할 때 마다 생성
      • 합산 후의 메모리 위치가 달리지는 이유
      • 합 연산마다 새로운 문자열이 계속 생성되어 메모리 낭비
    • string의 합 연산이 빈번한 경우, strings.Builder을 이용하는 것이 효율적이다
package main

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

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'))
		} else {
			builder.WriteRune(c)
		}
	}
	return builder.String()
}

func main() {
	var str string = "Hello"
	stringheader := (*reflect.StringHeader)(unsafe.Pointer(&str))
	addr1 := stringheader.Data

	str += " World"
	addr2 := stringheader.Data

	str += " Welcome!"
	addr3 := stringheader.Data

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

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

// Hello World Welcome!
// addr1:	4953ef       --> 최초의 str 변수 위치
// addr2:	c000012040   --> 문자열 추가후의 str 변수 위치
// addr3:	c00001a018   --> 문자열 추가후의 str 변수 위치
// HELLO WORLD WELCOME! --> 합 연산으로 생성
// HELLO WORLD WELCOME! --> strings.Builder로 생성
  • 문자열 불변의 법칙을 유지하는 이유
    • 문자열 자체는 구조체이므로 복사를 하면 하나의 문자 리터럴을 여러개의 문자열이 가리킴
    • 불변 법칙이 없다면 일부 데이터의 수정이 가능하면, 하나의 문자열 변수에 수정 다른 문자열 변수에도 영향을 끼침 → 결국 영향을 없어야 하는 곳도 영향이 생김
    • 문자열 불변의 법칙으로 같은 문자를 가리키는 문자열은 항상 같게 유지
profile
인프라 엔지니어

0개의 댓글