golang 스터디 3주차(for문 / 배열 / 구조체 / 포인터 / 문자열)

2023-11-26
22min
Golang

Chapter 11. for문

for 문은 아래처러 사용한다.

go
for 초기문; 조건문; 후처리 { // code block }
go
for 초기문; 조건문; 후처리 { // code block }
  • for 문이 실행 될 때 초기문이 먼저 실행됨
    • code block 이 실행되기 전에 조건문이 먼저 실행된다.
    • 따라서 for a:= 3; c < 3 c++ { 로 시작하는 for 문은 코드블록이 실행될 일이 없다.

11.1 for문 동작 원리

golang 의 for 문은 다양한 형태로 사용될 수 있다. 개인적으로 for 문을 이용해, 다른 언어의 while 처럼 동작하게 만드는 심플함이 마음에 든다.


11.1.1 초기문 생략


go
for ; 조건문 ; 후처리 { // }
go
for ; 조건문 ; 후처리 { // }

11.1.2 후처리 생략


go
for ; 조건문 ; 후처리 { // }
go
for ; 조건문 ; 후처리 { // }

11.1.3 조건문만 있는 경우

while문


go
for 조건문 { // }
go
for 조건문 { // }

11.2 break 와 continue

continue 와 break 는 반복문을 제어하는 키워드


go
for i := 0; i < 10; i++ { if i == 3 { continue } if i == 6 { break } }
go
for i := 0; i < 10; i++ { if i == 3 { continue } if i == 6 { break } }

INFO

중첩되어있는 for 문에서 break 와 continue 는 어떻게 동작할까??

정답은 break / continue 가 사용된 for 문만 제어된다!

Chapter 12. 배열

배열은 length 가 정해져있다.

배열은 같은 타입의 데이터들로 이루어진 타입이다. 배열을 이루는 각 값을 요소라하고 요소를 가리키는 위치값을 인덱스라고 한다.

var 변수명 [요소 개수]타입 와 같은 방법으로 선언한다.

go
var nums := [3]int{1,2,3} // 이렇게 초기화 할 수 있다. // NOTE: 특정 인덱스의 값만을 초기화하고, 나머지는 디폴트 초기화를 하고 싶다면 아래처럼 하면 된다. var specific := [3]int{1:100} // [0, 100, 0]
go
var nums := [3]int{1,2,3} // 이렇게 초기화 할 수 있다. // NOTE: 특정 인덱스의 값만을 초기화하고, 나머지는 디폴트 초기화를 하고 싶다면 아래처럼 하면 된다. var specific := [3]int{1:100} // [0, 100, 0]

배열 선언 시 개수는 상수라는 점에 주의해야한다. 즉 배열은 한번 만들어지면, 그 배열 자체는 늘어날 수 없다. 또한 변수 값을 이용해서 배열의 개수를 초기화 할 수 없다. 아래를 보자.

go
a := 1 // a 는 변수 b := [a]int({100}) // 실패. a 는 상수여야한다.
go
a := 1 // a 는 변수 b := [a]int({100}) // 실패. a 는 상수여야한다.

12.2 배열 사용법

range 를 이용한 순회방법. (for 문과 index 를 사용해서 순회가능. but 다루지 않음)

range 를 사용하면 이터레이터를 얻는듯. 따라서 아래처럼 사용할 수 있다. 주의할점은, 각 순회 마다 나오는 값은 2개이고, 첫번째 값은 배열의 값이 아니라 인덱스라는점

go
a := int[3]{1,2,3} for index, value := range a { fmt.Printf("index: %v, value: %v\n", index, value) }
go
a := int[3]{1,2,3} for index, value := range a { fmt.Printf("index: %v, value: %v\n", index, value) }

12.3 배열은 연속된 메모리

배열을 선언하면 컴퓨터는 연속된 메모리 공간을 확보한다.

예를들면 a := [3]int32{1,2,3} 이 선언되면 int32 크기의 3개 만큼의 공간이 할당된다는것. 즉 여기서는 4바이트 3개니깐 12바이트가 메모리에 할당된다.

배열은 연속된 메모리이고, 인덱스와 크기를 사용해서 메모리 주소를 찾는다! (a 를 이용해 배열의 시작점을 알 수 있고 각 인덱스 값의 크기를 알기 때문에 몇번째 인덱스의 값이 어느 메모리 공간에 있는지 계산할 수 있음)

12.3.1 배열의 복사

go
a := [3]{1,2,3} // 1, 2, 3 b := [3]{100,200,300} // 100, 200, 300 fmt.Printf("%p\n", &b) fmt.Printf("%p\n", &b[0]) b = a // b => 1, 2, 3 b[0] = 100 // b => 100, 2, 3 // NOTE: 할당한다고 해서 주소가 달라지지 않는다. 왜냐면, 하나의 값 그 자체기 때문. fmt.Printf("%p\n", &b) fmt.Printf("%p\n", &b[0]) // NOTE: a 는 어떻게 될까? 정답은..... a 는 변하지 않는다.
go
a := [3]{1,2,3} // 1, 2, 3 b := [3]{100,200,300} // 100, 200, 300 fmt.Printf("%p\n", &b) fmt.Printf("%p\n", &b[0]) b = a // b => 1, 2, 3 b[0] = 100 // b => 100, 2, 3 // NOTE: 할당한다고 해서 주소가 달라지지 않는다. 왜냐면, 하나의 값 그 자체기 때문. fmt.Printf("%p\n", &b) fmt.Printf("%p\n", &b[0]) // NOTE: a 는 어떻게 될까? 정답은..... a 는 변하지 않는다.

위의 예제 코드에서 a 를 b 에 할당하고 b[0] 의 값을 변경했다. js 를 주로 사용하는 내가 생각할 때는 당연히 a[0] 또한 100 으로 되어야할거같지만,

그렇지 않다. a 의 값은 변함없다. 왜?

a 를 b에 할당하는 순간, 변수 b 의 주소에 있는 배열에 a 의 값이 복사되어 들어오기 때문.

Chapter 13. 구조체

하나의 구조체는 여러 필드가 묶여있는 형태다. 배열이 같은 타입의 값들을 변수 하나로 묶어줬던 것과 달리 구조체는 다른 타입의 값들을 하나의 변수로 묶는다.

go
type 타입명 struct { 필드명 타입 ... 필드명 타입 }
go
type 타입명 struct { 필드명 타입 ... 필드명 타입 }
  1. type 키워드를 통해 새로운 사용자 정의 타입을 생성
  2. 타입명의 첫 번째 글자가 대문자면 외부로 공개되는 타입.
  3. 타입의 종류.
go
type Student struct { Name string Class int No int Score float64 } var a Student ### 13.2 구조체 변수 초기화 구조체 초기화는 중괄호 사이에 모든 필드값을 넣음으로 써 할수있다. ```go var house House = House{"anyang", 100, 200, "hello"}
go
type Student struct { Name string Class int No int Score float64 } var a Student ### 13.2 구조체 변수 초기화 구조체 초기화는 중괄호 사이에 모든 필드값을 넣음으로 써 할수있다. ```go var house House = House{"anyang", 100, 200, "hello"}

만약 일부 필드만 초기화를 하고 싶을 땐, 필드명: 필드값 형식으로 초기화를 진행하자.

go
var house House = House{ Address: "anyang" }
go
var house House = House{ Address: "anyang" }

만약 선언부에서 초기화를 진행하지 않았다면 기본값으로 초기화 된다. (int 는 0으로, string 은 '' 으로..)

13.3 구조체를 포함하는 구조체

구조체의 필드로 다른 구조체를 포함할 수 있다. 2가지 형식이 존재하는데, 기존의 방법과 Embedded Field 방식이 있다.

먼저 기존의 방법인 필드명: 필드값 형식으로...

go
package main import ( "fmt" ) type User struct { Name string ID string Age int } type VIPUser struct { UserInfo User VIPLevel int Price int } func main() { user := User{ "hello", "100", 30, } vipUser := VIPUser{ User{ "vipuser", "10", 100, }, 3, 100, } fmt.Println(user) fmt.Println(vipUser.UserInfo.Age) // vip user 의 age 를 출력하려면 2단계의 depth 가 있음.... }
go
package main import ( "fmt" ) type User struct { Name string ID string Age int } type VIPUser struct { UserInfo User VIPLevel int Price int } func main() { user := User{ "hello", "100", 30, } vipUser := VIPUser{ User{ "vipuser", "10", 100, }, 3, 100, } fmt.Println(user) fmt.Println(vipUser.UserInfo.Age) // vip user 의 age 를 출력하려면 2단계의 depth 가 있음.... }

기본적인 방법으로 생성하게 되면 맨 아래처럼 구조체인 필드 값을 가져오기 위해 . 연산자를 여러번 사용해야하는 불편함이 존재한다. 이 불편함을 해소할 수 있는 방법이 embedded

go
package main import ( "fmt" ) type User struct { Name string ID string Age int } type VIPUser struct { User VIPLevel int Price int } func main() { user := User{ "hello", "100", 30, } vipUser := VIPUser{ User{ "vipuser", "10", 100, }, 3, 100, } fmt.Println(user) fmt.Println(vipUser.Age) }
go
package main import ( "fmt" ) type User struct { Name string ID string Age int } type VIPUser struct { User VIPLevel int Price int } func main() { user := User{ "hello", "100", 30, } vipUser := VIPUser{ User{ "vipuser", "10", 100, }, 3, 100, } fmt.Println(user) fmt.Println(vipUser.Age) }

하이라이트 된 부분을 보자. 구조체의 선언부에 포함될 구조체는 Field 를 명시하지 않는다. 이렇게 함으로써 embedded 구조체가 되었고, nested 된 필드에 접근할 때 아까보다 쉽게 접근이 가능해졌다.

13.4 구조체 크기

구조체 변수가 선언되면 컴퓨터는 구조체 필드를 모두 담을 수 있는 메모리 공간을 할당한다. 크기를 구하는 방법은 간단하다. 내부 필드의 타입을 보고 결정.

go
type User struct { Age int Score float64 }
go
type User struct { Age int Score float64 }

와 같은 구조체가 있을 때 User 라는 구조체의 크기는 8 + 8 로 16바이트가 된다.

13.4.1 구조체 값 복사

구조체의 값 복사는 배열과 동일하다. 값 자체가 복사 되기 때문에 복사로 생성된 구조체의 필드값을 변경해도 복사에 사용된 구조체의 필드는 변경되지 않는다.

go
func main() { user := User{ 30, 1.1, } copied := user copied.Age = 100 fmt.Println(user.Age) // NOTE: 30 변하지 않음 fmt.Println(copied.Age) // 100 }
go
func main() { user := User{ 30, 1.1, } copied := user copied.Age = 100 fmt.Println(user.Age) // NOTE: 30 변하지 않음 fmt.Println(copied.Age) // 100 }

13.4.2 필드 배치 순서에 따른 구조체 크기 변화

이전에 구조체의 크기는 구조체 각 필드 타입의 크기의 합이라고 이야기를했다. 하지만 이는 틀린 이야기다.

아래의 결과로써 틀렸다는걸 알 수 있다. 그러면 왜? 바로 메모리 정렬 때문.

go
package main import ( "fmt" "unsafe" ) type User struct { Age int32 Score float64 } func main() { user := User{1, 1} fmt.Printf("size: %d\n}", unsafe.Sizeof(user)) }
go
package main import ( "fmt" "unsafe" ) type User struct { Age int32 Score float64 } func main() { user := User{1, 1} fmt.Printf("size: %d\n}", unsafe.Sizeof(user)) }

13.4.3 메모리 정렬

컴퓨터에서 연산은 레지스터에서 이뤄진다. 그리고 이 레지스터는 한번에 8바이트의 크기를 처리할 수 있다. 따라서 우리는 이 8 바이트 크기에 맞춰야하는것.

4바이트 공간은 4바이트가 추가로 더해지는 메모리 패딩이 발생함. 따라서 복잡한 구조체인 경우 메모리 패딩을 고려해서 구조체를 만들어야한다.

go
package main import ( "fmt" "unsafe" ) type User struct { Age int32 Score float64 } func main() { user := User{1, 2} fmt.Printf("할당된 크기: %v\n", unsafe.Sizeof(user)) fmt.Printf("Age의 주소: %p\n", &user.Age) // 0x14000112020 fmt.Printf("Score의 주소: %p\n", &user.Score) // 0x14000112028 }
go
package main import ( "fmt" "unsafe" ) type User struct { Age int32 Score float64 } func main() { user := User{1, 2} fmt.Printf("할당된 크기: %v\n", unsafe.Sizeof(user)) fmt.Printf("Age의 주소: %p\n", &user.Age) // 0x14000112020 fmt.Printf("Score의 주소: %p\n", &user.Score) // 0x14000112028 }

Chapter 14. 포인터


14.1 포인터란?


포인터는 메모리 주소를 값으로 갖는 타입이다.


14.1.1 포인터 변수 선언

var p *int 처럼 * 를 붙인다.

포인터는 메모리 주소를 값으로 갖는다. 메모리 주소를 가져오기 위해서는 & 연산자를 사용하면 된다.

go
package main import "fmt" func main() { a := 10 b := &a fmt.Printf("address of a: %p\n", &a) fmt.Printf("value of b: %v\n", b) fmt.Printf("value of b's address: %v\n", *b) fmt.Printf("address of b: %v\n", &b) }
go
package main import "fmt" func main() { a := 10 b := &a fmt.Printf("address of a: %p\n", &a) fmt.Printf("value of b: %v\n", b) fmt.Printf("value of b's address: %v\n", *b) fmt.Printf("address of b: %v\n", &b) }

위 코드에서 ba 값의 주소를 값으로 갖는다.

  • 9번째 라인의 결과와 10번쨰 라인의 결과는 같다.
  • 10번쨰 라인과 12번째 라인의 결과는 다르다. 왜? 12번째 라인은 b 의 주소기때문.

14.2 포인터는 왜 사용되나?

변수 대입이나 함수 인수 전달은 항상 인수 값을 복사하기 때문에 많은 메모리 공간이 사용된다. 또한 다른 주소의 값이기 때문에 값을 변경해도 적용되지 않는다.

따라서 포인터를 사용해 같은 주소를 가리키는 변수를 변경하도록 만들면, 큰 메모리 공간이 전부 복사 되지도 않고 동일한 값을 바라보기 때문에 값의 변경 또한 적용이 된다.

go
package main import "fmt" type User struct { Age int } func main() { user := &User{10} fmt.Printf("%v\n", user) p1 := user p2 := user p3 := user fmt.Printf("user: %v\n", user) fmt.Printf("p1's value: %v\n", p1) fmt.Printf("p2's value: %v\n", p2) fmt.Printf("p3's value: %v\n", p3) fmt.Printf("user age field address: %p\n", &user.Age) fmt.Printf("p1 age field address: %p\n", &p1.Age) fmt.Printf("p2 age field address: %p\n", &p2.Age) fmt.Printf("p3 age field address: %p\n", &p3.Age) fmt.Printf("user's address: %v\n", &user) fmt.Printf("p1's address: %v\n", &p1) fmt.Printf("p2's address: %v\n", &p2) fmt.Printf("p3's address: %v\n", &p3) }
go
package main import "fmt" type User struct { Age int } func main() { user := &User{10} fmt.Printf("%v\n", user) p1 := user p2 := user p3 := user fmt.Printf("user: %v\n", user) fmt.Printf("p1's value: %v\n", p1) fmt.Printf("p2's value: %v\n", p2) fmt.Printf("p3's value: %v\n", p3) fmt.Printf("user age field address: %p\n", &user.Age) fmt.Printf("p1 age field address: %p\n", &p1.Age) fmt.Printf("p2 age field address: %p\n", &p2.Age) fmt.Printf("p3 age field address: %p\n", &p3.Age) fmt.Printf("user's address: %v\n", &user) fmt.Printf("p1's address: %v\n", &p1) fmt.Printf("p2's address: %v\n", &p2) fmt.Printf("p3's address: %v\n", &p3) }

위 결과를 통해 user, p1, p2, p3 모두 같은 구조체를 바라본다는 것을 알 수 있다. 만약 p1 를 이용해 구조체의 값을 변경하면 다른 포인터에서의 값 출력에서도 다 변경된다.

Chapter 15. 문자열

문자열은 문자의 집합이다. 큰따옴표나 백쿼트로 묶어서 표시한다. 큰따옴표와 백쿼트는 특수문자를 다루는데에 있어 차이가 있다.

go
str := "Hello\t world"; str2 := `Hello\t world`; fmt.Println(str) // Hello world fmt.Println(str2) // Hello\t world
go
str := "Hello\t world"; str2 := `Hello\t world`; fmt.Println(str) // Hello world fmt.Println(str2) // Hello\t world

큰따옴표는 특수문자를 처리하는 반면 백쿼느틑 특수문자를 처리하지 않는다.

INFO

큰또옴표를 사용해도 특수문자를 표시할 수 있는 방법이 있다.

go
str3 := "Hello\\t world" fmt.Println(str3) // Hello\t world
go
str3 := "Hello\\t world" fmt.Println(str3) // Hello\t world

15.1 문자열


15.1.1 UTF-8 문자코드

Go 는 UTF-8 문자코트드를 표준 문자코트로 사용한다. UTF-16은 한 문자에 2바이트를 고정으로 사용하는 반면 UTF-8은 영문자, 숫자, 일부 특수 ㅜㅁㄴ자를 1바이트로 표현하고 그외 다른 문자들은 2 ~ 3 바이트로 표현한다. 덕분에 한글이나 한자등을 변환 할 필요없이 사용할 수 있다.

15.1.2 rune 타입으로 한 문자 담기

문자 하나를 표현하는 데 rune 타입을 사용한다. UTF-8 은 한 글자가 1 ~ 3 바이트 크기이기 때문에 UTF-8 문자값을 가지려면 3바이트가 필요하다.

rune 타입은 4바이트 정수 타입인 int32 의 타입 별칭이다. type rune int32

15.1.3 len() 으로 문자열 크기 알아내기

len() 내장 함수를 이용해 문자열 크기를 알 수 있다. 문자의 수가 아니라 문자열이 차지하는 메모리 크기

go
package main import ( "fmt" ) func main() { str := "12345" str2 := "안녕하세요" fmt.Println(len(str)) // 5 fmt.Println(len(str2)) // 15 }
go
package main import ( "fmt" ) func main() { str := "12345" str2 := "안녕하세요" fmt.Println(len(str)) // 5 fmt.Println(len(str2)) // 15 }

한글은 문자 당 3바이트를 차지하므로 안녕하세요 는 15바이트의 결과값이 나오게 된다.

15.1.5 string 타입을 []byte로 타입 변환

string 타입, rune 슬라이스 타입인 []rune 타입은 상호 타입 변환이 가능하다.

  1. []rune 타입을 string 으로 변환
    • go
      runes := []rune{72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100 } str := string(runes)
      go
      runes := []rune{72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100 } str := string(runes)
  2. string 타입을 rune[] 으로 변환
    • go
      str := "Hello world" runes := []rune(str)
      go
      str := "Hello world" runes := []rune(str)

15.2 문자열 순회

  1. 인덱스를 사용한 바이트 단위 순회
  2. []rune 으로 타입 변환 후 한 글자씩 순회
  3. range 키워드를 이용한 한 글자씩 순회

15.2.1 인덱스를 사용해 바이트 단위 순회

go
package main import ( "fmt" ) func main() { str := "hello 월드" for i := 0; i < len(str); i++ { fmt.Printf("%c", str[i]) } }
go
package main import ( "fmt" ) func main() { str := "hello 월드" for i := 0; i < len(str); i++ { fmt.Printf("%c", str[i]) } }

결과값: hello 월드

결과를 보면 이상한 문자가 보인다. 원인은 글자 단위가 아닌 바이트로 쪼개서 보여줬기 때문이다.

15.2.2 []rune 으로 타입 변환 후 한 글자씩 순회하기

정상적으로 잘 나온다. 예시는 생략.

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

go
package main import ( "fmt" ) func main() { str := "hello 월드" for _, v := range str { fmt.Printf("%c", v) } }
go
package main import ( "fmt" ) func main() { str := "hello 월드" for _, v := range str { fmt.Printf("%c", v) } }

15.4 문자열 구조


15.4.1 string 구조 알아보기

string 타입은 Go 언어에서 제공하는 내장 타입으로, 그 내부 구현은 감추어져 있다. reflect 패키지안의 StringHeader 구조체를 보면 엿볼 수 있다.

go
type StringHeader struct { Data uintptr Len int } type uintptr uintptr // uintptr is an integer type that is large enough to hold the bit pattern of any pointer.
go
type StringHeader struct { Data uintptr Len int } type uintptr uintptr // uintptr is an integer type that is large enough to hold the bit pattern of any pointer.

string 은 필드가 2개인 구조체다.

  • Data: 문자열의 주소를 가리킨다.
  • Len: 문자열의 길이를 나타낸다.

15.4.2 string 대입

go
package main import ( "fmt" "reflect" "unsafe" ) func main() { str1 := "hello world" str2 := str1 fmt.Println(&str1) fmt.Println(&str2) strHeader1 := (*reflect.StringHeader)(unsafe.Pointer(&str1)) strHeader2 := (*reflect.StringHeader)(unsafe.Pointer(&str2)) fmt.Println(strHeader1) fmt.Println(strHeader2) fmt.Println(strHeader1.Data) fmt.Println(strHeader2.Data) fmt.Printf("%v\n", strHeader1.Data) fmt.Printf("%v\n", strHeader2.Data) }
go
package main import ( "fmt" "reflect" "unsafe" ) func main() { str1 := "hello world" str2 := str1 fmt.Println(&str1) fmt.Println(&str2) strHeader1 := (*reflect.StringHeader)(unsafe.Pointer(&str1)) strHeader2 := (*reflect.StringHeader)(unsafe.Pointer(&str2)) fmt.Println(strHeader1) fmt.Println(strHeader2) fmt.Println(strHeader1.Data) fmt.Println(strHeader2.Data) fmt.Printf("%v\n", strHeader1.Data) fmt.Printf("%v\n", strHeader2.Data) }

결과를 보면 strHeader1 과 strHeader2 같은 주소를 가리키고 있다. 즉, 하나의 문자열을 다른 문자열로 대입하면 문자열 전체가 복사되는 것이 아닌, 변수를 담는 공간 하나만 생성된다. 따라서 성능 / 메모리 걱정을 할 필요가 없다. (배열은 대입연산에 의해 전체가 복사 되는 문제가 생김)

15.5 문자열은 불변이다.

문자열은 불변이다. 따라서 문자열의 일부만 변경할 수는 없다.

그러면 문자열의 변경은 어떻게 해야해? 문자열변수가 가리키는 메모리 주소를 변경하면 된다.

go
package main import ( "fmt" "reflect" "unsafe" ) func main() { str := "hello world" strHeaderBefore := (*reflect.StringHeader)(unsafe.Pointer(&str)) fmt.Println(strHeaderBefore) str = "what?" strHeaderAfter := (*reflect.StringHeader)(unsafe.Pointer(&str)) fmt.Println(strHeaderAfter) }
go
package main import ( "fmt" "reflect" "unsafe" ) func main() { str := "hello world" strHeaderBefore := (*reflect.StringHeader)(unsafe.Pointer(&str)) fmt.Println(strHeaderBefore) str = "what?" strHeaderAfter := (*reflect.StringHeader)(unsafe.Pointer(&str)) fmt.Println(strHeaderAfter) }

결과를 보면 아래 처럼 나오게 된다. 즉, 문자열을 변경하려면 다른 공간의 문자열을 넣어야한다.

&{4329437816 11} &{4329435317 5}
&{4329437816 11} &{4329435317 5}

위의 특성은 문자열의 합산에서도 그대로 나타난다.

go
package main import ( "fmt" "reflect" "unsafe" ) func main() { str := "hello" strHeader := (*reflect.StringHeader)(unsafe.Pointer(&str)) addr1 := strHeader.Data str += " world" addr2 := strHeader.Data str += " foo bar" addr3 := strHeader.Data fmt.Printf("addr1: %x\n", addr1) fmt.Printf("addr1: %x\n", addr2) fmt.Printf("addr1: %x\n", addr3) }
go
package main import ( "fmt" "reflect" "unsafe" ) func main() { str := "hello" strHeader := (*reflect.StringHeader)(unsafe.Pointer(&str)) addr1 := strHeader.Data str += " world" addr2 := strHeader.Data str += " foo bar" addr3 := strHeader.Data fmt.Printf("addr1: %x\n", addr1) fmt.Printf("addr1: %x\n", addr2) fmt.Printf("addr1: %x\n", addr3) }

문자열을 합산 할 때 마다 새로운 메모리 공간을 만들어 문자열을 합치고, 연산 이후 새로운 공간을 갖게 된다. 따라서 문자열 불변의 원칙은 준수된다.

문제는 이런 문자열 합산을 하게 되면 매번 새로운 공간을 만들게 되므로 메모리 낭비가 생긴다. 만약 문자열 합산을 빈번하게 해야하는 상황이 온다면 Builder 를 이용하면 된다.

go
package main import ( "fmt" "strings" ) func main() { var builder strings.Builder // NOTE: method chaning builder.WriteString("Hello, ") builder.WriteString("world!") result := builder.String() fmt.Println(result) }
go
package main import ( "fmt" "strings" ) func main() { var builder strings.Builder // NOTE: method chaning builder.WriteString("Hello, ") builder.WriteString("world!") result := builder.String() fmt.Println(result) }

strings 패키지의 builder 는 뭐가 다르길래 메모리 낭비가 되지 않는다는걸까?

참고1

Builder 를 살펴보자. Builder 구조체는 addr 필드와 buf 필드를 가지고 있다.

go
type Builder struct { addr *Builder // of receiver, to detect copies by value buf []byte }
go
type Builder struct { addr *Builder // of receiver, to detect copies by value buf []byte }
  • addr: 자기 자신의 주소를 가리킴
  • buf: 문자열을 임시로 저장하기 위한 버퍼입니다.

buf 라는 byte 타입의 슬라이스에 합치는 문자열을 임시 저장함으로써 문자열을 함칩할 때 마다 메모리 공간의 할당이 일어나지 않는다.

그렇다고해서 메모리 재할당이 아예 안일어나는건 아니다. 슬라이스의 cap 을 넘어서는 경우 재할당을 한다.

15.5.2 왜 문자열은 불변 원칙을 지키려 할까?

위의 예제에서 보았다시피 문자열의 합 연산 마다 메모리가 계속 낭비되게 된다. 그럼에도 불구하고 왜? 불변 원칙을 지키는가? 이유는 예기지 못한 버그를 방지하기 위해서다.