Go의 pointer는 값이 저장된 메모리 주소를 담습니다. pointer를 사용하면 함수가 원본 변수의 값을 직접 변경할 수 있지만, Go에서는 C나 C++처럼 주소에 숫자를 더하거나 빼는 포인터 연산(pointer arithmetic)을 허용하지 않습니다.
Go의 string은 변경할 수 없는 byte sequence입니다. 문자열 리터럴은 일반적으로 UTF-8로 작성하지만, string 값 자체가 항상 올바른 UTF-8이라는 보장은 없습니다. 따라서 문자열을 다룰 때는 byte, rune, Unicode code point, 사용자가 화면에서 하나로 인식하는 문자 단위를 구분해야 합니다.
Unicode code point(유니코드 문자번호)는 Unicode에서 각 문자나 기호에 부여한 고유한 번호를 뜻합니다.
핵심 정리
| 구분 | 의미 |
|---|---|
| pointer | 값이 저장된 메모리 주소를 담는 값 |
&value |
변수 value의 주소를 구함 |
*pointer |
pointer가 가리키는 값을 읽거나 변경함 |
nil pointer |
아직 유효한 값을 가리키지 않는 pointer |
string |
변경할 수 없는 byte sequence |
byte |
uint8의 별칭으로, 문자열의 개별 byte를 표현할 때 주로 사용 |
rune |
int32의 별칭으로, Unicode code point 하나를 표현 |
len(s) |
문자열의 byte 수 |
utf8.RuneCountInString(s) |
UTF-8을 해석했을 때의 rune 수 |
range s |
UTF-8을 decode하여 byte index와 rune을 순회 |
len(string)이 반환하는 값은 화면에 보이는 문자 수가 아닙니다. 한글이나 emoji처럼 UTF-8에서 여러 byte를 사용하는 문자가 포함되면 byte 수와 rune 수가 달라집니다.
디렉터리 구조는 다음과 같이 구성합니다.
sources/
└── pointers-strings/
└── main.gosources 디렉터리에서 실행합니다.
cd sources
go run ./pointers-stringsPointer로 원본 값 다루기
일반 변수를 다른 변수에 대입하면 값이 복사됩니다.
original := 10
copied := original
copied = 20
fmt.Println(original) // 10
fmt.Println(copied) // 20copied를 변경해도 original은 바뀌지 않습니다.
pointer에는 값 자체가 아니라 값이 저장된 주소를 담습니다.
score := 70
scorePointer := &score
fmt.Println(score) // 70
fmt.Println(scorePointer) // score가 저장된 주소
fmt.Println(*scorePointer) // 70각 연산자의 의미는 다음과 같습니다.
| 표현 | 의미 |
|---|---|
&score |
score가 저장된 주소 |
scorePointer |
score의 주소를 담은 pointer |
*scorePointer |
pointer가 가리키는 실제 값 |
pointer를 역참조하여 값을 변경하면 원본 변수도 변경됩니다.
score := 70
scorePointer := &score
*scorePointer = 90
fmt.Println(score) // 90함수에서 원본 값 변경하기
Go는 함수에 인수를 전달할 때 값을 복사합니다. 일반 값을 전달하면 함수 내부의 변경이 호출한 쪽의 변수에 반영되지 않습니다.
func updateValue(value int) {
value = 100
}
func main() {
score := 70
updateValue(score)
fmt.Println(score) // 70
}원본 값을 변경해야 한다면 해당 변수의 pointer를 전달할 수 있습니다.
func updateValue(value *int) {
*value = 100
}
func main() {
score := 70
updateValue(&score)
fmt.Println(score) // 100
}updateValue가 받는 것도 pointer 값의 복사본입니다. 다만 복사된 pointer가 호출한 쪽 변수와 같은 주소를 가리키므로, 역참조를 통해 원본 값을 변경할 수 있습니다.
pointer가 필요하다는 이유만으로 모든 값을 pointer로 전달할 필요는 없습니다. 원본을 변경해야 하거나 큰 구조체의 복사 비용을 줄여야 하는 등 명확한 이유가 있을 때 사용하는 것이 좋습니다.
nil pointer 확인하기
pointer 타입의 zero value는 nil입니다.
var scorePointer *int
fmt.Println(scorePointer == nil) // truenil pointer를 역참조하면 실행 중 panic이 발생합니다.
var scorePointer *int
fmt.Println(*scorePointer) // panic따라서 pointer가 nil일 수 있다면 먼저 확인해야 합니다.
func printValue(value *int) {
if value == nil {
fmt.Println("값이 없습니다.")
return
}
fmt.Println(*value)
}포인터 연산(Pointer arithmetic)을 사용할 수 없는 이유
Go에서는 다음과 같은 연산을 사용할 수 없습니다.
pointer++
pointer = pointer + 1pointer에 정수를 더해 임의의 메모리 위치로 이동하는 연산은 허용되지 않습니다. 배열이나 연속된 데이터를 순회하려면 index, range, slice를 사용합니다.
numbers := []int{10, 20, 30}
for index, number := range numbers {
fmt.Printf("index=%d, number=%d\n", index, number)
}String은 변경할 수 없는 byte sequence
Go의 string은 byte들의 연속이며 한 번 만들어진 문자열의 내용을 직접 변경할 수 없습니다.
message := "Go"다음 코드는 컴파일되지 않습니다.
message[0] = 'N'문자열 index로 얻은 값은 byte이며, string의 특정 byte를 직접 바꿀 수 없기 때문입니다.
문자열을 변경하려면 []byte 또는 []rune으로 변환한 뒤 새 string을 만들어야 합니다.
ASCII 범위의 byte 변경
message := "Go"
bytes := []byte(message)
bytes[0] = 'N'
message = string(bytes)
fmt.Println(message) // No유니코드 문자번호(Unicode code point) 단위 변경
message := "가나다"
runes := []rune(message)
runes[0] = '라'
message = string(runes)
fmt.Println(message) // 라나다한글처럼 여러 byte로 인코딩되는 문자를 byte 단위로 임의 변경하면 UTF-8이 손상될 수 있습니다. Unicode 문자를 code point 단위로 변경하려면 []rune 변환이 더 적절합니다.
문자열 대입은 내용을 수정하는 것이 아니다
first := "hello"
second := first
second = "world"
fmt.Println(first) // hello
fmt.Println(second) // worldsecond에 새 문자열을 대입해도 first의 내용은 변경되지 않습니다. string 값은 변경할 수 없으며, 대입은 문자열 값을 다른 변수에 복사하는 동작입니다.
Byte와 rune의 차이
byte는 uint8의 별칭이고, rune은 int32의 별칭입니다.
var b byte = 'A'
var r rune = '한'
fmt.Printf("%d %c\n", b, b)
fmt.Printf("%U %c\n", r, r)출력:
65 A
U+D55C 한'A'와 '한'처럼 작은따옴표로 작성한 값은 rune literal입니다. 문자 하나의 유니코드 문자번호(Unicode code point) 값을 나타냅니다.
반면 "A"와 "한"처럼 큰따옴표로 작성한 값은 string literal입니다.
fmt.Printf("%T\n", 'A') // int32
fmt.Printf("%T\n", "A") // string문자열 index는 byte를 반환한다
text := "한글"
fmt.Printf("%T\n", text[0])
fmt.Printf("%x\n", text[0])text[0]은 첫 번째 문자인 한을 반환하지 않습니다. 한을 UTF-8로 인코딩한 첫 번째 byte를 반환합니다.
문자열의 유니코드 문자번호(Unicode code point)를 index로 다루려면 먼저 []rune으로 변환할 수 있습니다.
text := "한글"
runes := []rune(text)
fmt.Printf("%c\n", runes[0]) // 한
fmt.Printf("%c\n", runes[1]) // 글다만 []rune 변환은 새로운 slice를 만들고 문자열 전체를 decode합니다. 단순 순회가 목적이라면 range가 더 자연스럽습니다.
len, range, rune 수의 차이
다음 문자열에는 영문자 2개, 한글 2개, emoji 1개가 들어 있습니다.
text := "Go한글🙂"UTF-8에서 각 문자가 차지하는 byte 수는 다음과 같습니다.
| 값 | UTF-8 byte 수 |
|---|---|
G |
1 |
o |
1 |
한 |
3 |
글 |
3 |
🙂 |
4 |
| 합계 | 12 |
따라서 len과 rune 수의 결과가 다릅니다.
fmt.Println(len(text))
fmt.Println(utf8.RuneCountInString(text))
fmt.Println(len([]rune(text)))출력:
12
5
5len(text)는 12개의 byte를 셉니다.utf8.RuneCountInString(text)은 UTF-8을 decode하여 5개의 rune을 셉니다.len([]rune(text))도 변환된 rune slice의 원소 수인 5를 반환합니다.
byte 단위 순회
일반 for와 index를 사용하면 문자열을 byte 단위로 순회합니다.
for index := 0; index < len(text); index++ {
fmt.Printf("index=%d byte=%x\n", index, text[index])
}한글이나 emoji는 여러 줄에 걸쳐 각각의 byte로 출력됩니다.
range로 rune 순회
문자열에 range를 사용하면 UTF-8을 decode하여 byte index와 rune을 반환합니다.
for index, character := range text {
fmt.Printf(
"byteIndex=%d rune=%c codePoint=%U\n",
index,
character,
character,
)
}출력:
byteIndex=0 rune=G codePoint=U+0047
byteIndex=1 rune=o codePoint=U+006F
byteIndex=2 rune=한 codePoint=U+D55C
byteIndex=5 rune=글 codePoint=U+AE00
byteIndex=8 rune=🙂 codePoint=U+1F642여기서 index는 몇 번째 문자인지가 아니라 해당 rune이 시작되는 byte 위치입니다.
한은 byte index 2에서 시작하고 3 byte를 사용하므로, 다음 rune인 글은 byte index 5에서 시작합니다.
rune의 순번도 필요하다면 별도의 변수를 사용할 수 있습니다.
runeIndex := 0
for byteIndex, character := range text {
fmt.Printf(
"runeIndex=%d byteIndex=%d rune=%c\n",
runeIndex,
byteIndex,
character,
)
runeIndex++
}String이 항상 유효한 UTF-8은 아니다
Go string에는 임의의 byte를 담을 수 있습니다. 따라서 string이 반드시 유효한 UTF-8로 구성된다는 보장은 없습니다.
invalidText := string([]byte{0x47, 0x6f, 0xff, 0x21})
fmt.Println(utf8.ValidString(invalidText)) // false0xff는 단독으로 올바른 UTF-8 문자를 구성하지 않습니다.
유효하지 않은 UTF-8 문자열을 range로 순회하면 잘못된 byte 위치에서 Unicode replacement character인 U+FFFD가 반환됩니다.
for index, character := range invalidText {
fmt.Printf(
"byteIndex=%d rune=%c codePoint=%U\n",
index,
character,
character,
)
}출력:
byteIndex=0 rune=G codePoint=U+0047
byteIndex=1 rune=o codePoint=U+006F
byteIndex=2 rune=� codePoint=U+FFFD
byteIndex=3 rune=! codePoint=U+0021외부 파일, 네트워크 요청, 데이터베이스 등에서 받은 문자열이 올바른 UTF-8이어야 한다면 utf8.ValidString으로 검증할 수 있습니다.
if !utf8.ValidString(input) {
return errors.New("유효하지 않은 UTF-8 문자열입니다")
}Rune 수와 화면에 보이는 문자 수는 다를 수 있다
rune은 유니코드 문자번호(Unicode code point) 하나를 의미합니다. 그러나 사용자가 화면에서 하나의 문자로 인식하는 단위가 항상 rune 하나인 것은 아닙니다.
예를 들어 é는 다음 두 방식으로 표현할 수 있습니다.
composed := "é"
decomposed := "e\u0301"두 문자열은 화면에서 비슷하게 보이지만 내부 표현은 다릅니다.
fmt.Println(len(composed))
fmt.Println(utf8.RuneCountInString(composed))
fmt.Println(len(decomposed))
fmt.Println(utf8.RuneCountInString(decomposed))출력:
2
1
3
2| 문자열 | 구성 | byte 수 | rune 수 |
|---|---|---|---|
é |
U+00E9 |
2 | 1 |
é |
U+0065 + U+0301 |
3 | 2 |
두 번째 문자열은 알파벳 e와 combining acute accent라는 두 code point로 구성되지만, 화면에서는 하나처럼 보일 수 있습니다.
여러 code point를 조합한 emoji도 같은 문제가 있습니다.
family := "👨👩👧👦"
fmt.Println(len(family)) // 25
fmt.Println(utf8.RuneCountInString(family)) // 7이 emoji는 여러 rune으로 구성되지만 일반적인 화면에서는 하나의 가족 emoji로 표시됩니다.
Unicode에서는 사용자가 하나로 인식하는 문자 단위를 grapheme cluster라고 부릅니다. Go 표준 라이브러리의 len이나 utf8.RuneCountInString은 grapheme cluster 수를 계산하지 않습니다.
사용자 이름 길이 제한, 입력 칸의 글자 수 표시, 화면 너비 계산처럼 사용자가 인식하는 문자 단위가 중요하다면 byte 수나 rune 수만으로 처리해서는 안 됩니다. Unicode grapheme cluster를 지원하는 별도의 처리 방식이 필요합니다.
전체 예제
아래 예제는 pointer, string, byte, rune, UTF-8 순회를 한 번에 확인합니다.
sources/pointers-strings/main.go:
package main
import (
"fmt"
"unicode/utf8"
)
func updateScore(score *int) {
if score == nil {
return
}
*score = 90
}
func main() {
score := 70
updateScore(&score)
fmt.Println("[pointer]")
fmt.Printf("score=%d\n", score)
var emptyPointer *int
fmt.Printf("emptyPointer is nil=%t\n", emptyPointer == nil)
text := "Go한글🙂"
fmt.Println("\n[string length]")
fmt.Printf("text=%q\n", text)
fmt.Printf("byte count=%d\n", len(text))
fmt.Printf("rune count=%d\n", utf8.RuneCountInString(text))
fmt.Println("\n[range]")
runeIndex := 0
for byteIndex, character := range text {
fmt.Printf(
"runeIndex=%d byteIndex=%d rune=%c codePoint=%U\n",
runeIndex,
byteIndex,
character,
character,
)
runeIndex++
}
fmt.Println("\n[string update]")
runes := []rune("가나다")
runes[0] = '라'
fmt.Printf("updated=%q\n", string(runes))
fmt.Println("\n[invalid UTF-8]")
invalidText := string([]byte{0x47, 0x6f, 0xff, 0x21})
fmt.Printf("valid=%t\n", utf8.ValidString(invalidText))
for byteIndex, character := range invalidText {
fmt.Printf(
"byteIndex=%d rune=%c codePoint=%U\n",
byteIndex,
character,
character,
)
}
fmt.Println("\n[rune and grapheme cluster]")
composed := "é"
decomposed := "e\u0301"
family := "👨👩👧👦"
fmt.Printf(
"composed=%q bytes=%d runes=%d\n",
composed,
len(composed),
utf8.RuneCountInString(composed),
)
fmt.Printf(
"decomposed=%q bytes=%d runes=%d\n",
decomposed,
len(decomposed),
utf8.RuneCountInString(decomposed),
)
fmt.Printf(
"family=%q bytes=%d runes=%d\n",
family,
len(family),
utf8.RuneCountInString(family),
)
}실행:
cd sources
go run ./pointers-strings출력:
[pointer]
score=90
emptyPointer is nil=true
[string length]
text="Go한글🙂"
byte count=12
rune count=5
[range]
runeIndex=0 byteIndex=0 rune=G codePoint=U+0047
runeIndex=1 byteIndex=1 rune=o codePoint=U+006F
runeIndex=2 byteIndex=2 rune=한 codePoint=U+D55C
runeIndex=3 byteIndex=5 rune=글 codePoint=U+AE00
runeIndex=4 byteIndex=8 rune=🙂 codePoint=U+1F642
[string update]
updated="라나다"
[invalid UTF-8]
valid=false
byteIndex=0 rune=G codePoint=U+0047
byteIndex=1 rune=o codePoint=U+006F
byteIndex=2 rune=� codePoint=U+FFFD
byteIndex=3 rune=! codePoint=U+0021
[rune and grapheme cluster]
composed="é" bytes=2 runes=1
decomposed="é" bytes=3 runes=2
family="👨👩👧👦" bytes=25 runes=7정리
pointer는 값의 주소를 저장하며, &로 주소를 얻고 *로 해당 주소의 값을 읽거나 변경합니다. 함수에 pointer를 전달하면 호출한 쪽의 원본 값을 변경할 수 있지만, nil pointer를 역참조하지 않도록 확인해야 합니다.
Go의 string은 변경할 수 없는 byte sequence입니다. 문자열 index는 문자가 아니라 byte를 반환하고, len도 문자 수가 아니라 byte 수를 반환합니다.
UTF-8 문자열을 유니코드 문자번호(Unicode code point) 단위로 순회하려면 range를 사용할 수 있습니다. 이때 index는 rune 순번이 아니라 rune이 시작하는 byte index입니다.
rune은 유니코드 문자번호 하나를 나타내지만, rune 수가 사용자가 화면에서 인식하는 문자 수와 항상 같지는 않습니다. 결합 문자나 복합 emoji는 여러 rune이 하나의 grapheme cluster로 표시될 수 있으므로, 사용자 입력 길이를 계산할 때는 어떤 단위가 필요한지 먼저 정해야 합니다.
참고 자료
'프로그래밍 > Go' 카테고리의 다른 글
| Go struct와 receiver (0) | 2026.07.15 |
|---|---|
| Go struct, method, interface, enum 패턴과 embedding (0) | 2026.07.15 |
| Go 함수, closure, recursion (1) | 2026.07.14 |
| Go 배열, slice, map, range (0) | 2026.07.13 |
| Go if, for, switch와 break, continue (0) | 2026.07.11 |
댓글