Go의 기본 내장 타입(predeclared types)을 정리한다. 리터럴 표기법부터 불리언, 그리고 유독 많은 숫자 타입까지 — Go가 다른 언어와 어떻게 다르게 접근하는지가 핵심이다.
1. Overview
Go 타입 & 선언
├── Predeclared Types
│ ├── Zero Value (선언만 하고 값을 안 주면 자동으로 채워지는 기본값)
│ ├── Literals Integer / Floating-Point / Rune / String (+ Imaginary)
│ ├── Boolean true / false, zero value = false
│ ├── Numeric Types Integer(8종) · Float(2종) · Complex(2종)
│ └── String & Rune 불변 문자열, rune = int32 별칭
├── 명시적 타입 변환 자동 승격 없음, bool 변환도 없음
├── 변수 선언
│ ├── var 타입 명시 / 추론 / zero value / 다중 선언
│ └── := 함수 내부 전용, 재할당 겸용, shadowing 주의
├── const 컴파일 타임 값에만 이름 붙이기, typed/untyped
├── 미사용 변수·상수 변수는 컴파일 에러, 상수는 허용
└── 네이밍 규칙 카멜케이스, 스코프 좁을수록 짧게
2. Zero Value
변수를 선언만 하고 값을 할당하지 않으면, Go는 항상 예측 가능한 기본값을 채워 넣는다. C/C++처럼 초기화 안 된 변수가 쓰레기 값을 갖는 것과 대조적이며, 이는 그 자체로 하나의 버그 클래스를 없애준다.
var flag bool // 값 할당 안 함 → 자동으로 false
3. Literals
Go 리터럴은 코드에 직접 박아넣는 숫자, 문자, 문자열을 말한다. 크게 네 종류(+ 복소수까지 다섯 번째)가 있다.
3.1 Integer Literals
기본은 10진수. 접두어로 다른 진법 표현이 가능하다.
| 접두어 | 진법 |
|---|---|
| (없음) | 10진수 (base 10) |
0b | 2진수 (binary) |
0o | 8진수 (octal) |
0x | 16진수 (hexadecimal) |
1_234 // 밑줄로 천 단위 구분, 값에는 영향 없음
0o777 // 8진수 — POSIX 권한 표기에 주로 사용 (rwxrwxrwx)
0x1F // 16진수
앞에
0만 붙이는 옛날식 8진수 표기(017)는 헷갈리므로 쓰지 않는다.
밑줄(_) 규칙: 시작/끝에 올 수 없고, 연속으로 붙일 수 없다. 10진수는 천 단위, 2/8/16진수는 1·2·4바이트 경계로 끊는 것이 관례다.
실무 가이드: 정수/부동소수점은 기본적으로 10진수를 쓴다. 8진수는 POSIX 권한(0o777) 표기에, 16진수·2진수는 비트 필터나 네트워킹/인프라 관련 코드에서 제한적으로 쓰인다.
3.2 Floating-Point Literals
6.03e23 // e + 지수 → 6.03 × 10^23
0x12.34p5 // 16진 부동소수점, p로 지수 표기 (= 582.5, 실무에서 거의 안 씀)
1_234.567_8 // 정수와 마찬가지로 밑줄로 포맷팅 가능
3.3 Rune Literals
작은따옴표로 문자 하나를 나타낸다. 큰따옴표(string)와 절대 혼용 불가하다는 점이 다른 언어와의 가장 큰 차이.
'a' // 유니코드 문자
'\141' // 8비트 8진수
'\x61' // 8비트 16진수
'\u0061' // 16비트 16진수
'\U00000061' // 32비트 유니코드
위 다섯 표기는 전부 동일한 문자 'a'를 가리킨다. 실무에서는 특수한 이유가 없다면 그냥 'a'처럼 직관적으로 쓴다.
자주 쓰는 이스케이프: '\n'(줄바꿈), '\t'(탭), '\''(작은따옴표), '\\'(백슬래시).
룬 리터럴의 이스케이프 중
'\''(작은따옴표 이스케이프)만은 문자열 리터럴에서 그대로 쓸 수 없다. 문자열은 큰따옴표로 감싸므로, 대신\"(큰따옴표 이스케이프)를 사용한다.
3.4 String Literals
해석된 문자열(interpreted) — 큰따옴표, 이스케이프가 실제로 동작:
"Greetings and\n\"Salutations\""
원시 문자열(raw) — 백틱, 이스케이프 없이 있는 그대로:
`Greetings and
"Salutations"`
백슬래시·큰따옴표·줄바꿈이 많이 들어가는 문자열(정규식, JSON 등)은 raw string이 훨씬 간편하다.
리터럴은 원래 타입이 없다(untyped).
x := 5처럼 타입을 명시하지 않으면, 문맥에 단서가 없을 경우 Go가 기본 타입(정수는int, 실수는float64)을 자동으로 부여한다.
4. Booleans
var flag bool // zero value → false
var isAwesome = true
true / false 두 값만 가지며, zero value는 false.
5. Numeric Types
Go는 12개 숫자 타입을 3개 카테고리(정수/부동소수점/복소수)로 나눠 제공한다. 단일 Number 타입만 있는 JavaScript 같은 언어에서 넘어오면 많아 보이지만, 실제로 자주 쓰는 건 소수다.
5.1 Integer Types
| 타입명 | 값의 범위 |
|---|---|
| int8 | –128 ~ 127 |
| int16 | –32768 ~ 32767 |
| int32 | –2147483648 ~ 2147483647 |
| int64 | –9223372036854775808 ~ 9223372036854775807 |
| uint8 | 0 ~ 255 |
| uint16 | 0 ~ 65535 |
| uint32 | 0 ~ 4294967295 |
| uint64 | 0 ~ 18446744073709551615 |
모든 정수 타입의 zero value는 0.
5.2 Special Integer Names
byte=uint8의 별칭. 실무에서는uint8대신 그냥byte라고 부른다.int= 플랫폼(CPU 아키텍처)에 따라 32비트 또는 64비트. 대부분의 64비트 시스템에서는int64와 동일 크기지만,amd64p32/mips64p32/mips64p32le같은 예외 아키텍처에서는 32비트.uint=int와 동일한 규칙, 부호만 없음(항상 0 이상).rune,uintptr— 각각 문자열/포인터 관련 별도 절에서 다룸.
int와int32/int64는 서로 다른 타입으로 취급되어, 명시적 타입 변환 없이 섞어 쓰면 컴파일 에러가 난다. 정수 리터럴은 별다른 문맥이 없으면 기본적으로int타입.
레거시 패턴: 제네릭이 없던 시절, strconv.FormatInt(int64, ...) / strconv.FormatUint(uint64, ...)처럼 부호 있는/없는 정수 각각에 대해 가장 큰 타입(int64, uint64) 하나씩만 만들어두고, 호출자가 타입 변환해서 쓰도록 설계한 API들이 표준 라이브러리에 남아있다.
5.3 Choosing Which Integer to Use
- 특정 크기/부호가 요구되는 바이너리 포맷·네트워크 프로토콜을 다룰 때 → 그에 맞는 타입 사용
- 여러 정수 타입에 두루 동작해야 하는 라이브러리 함수 → 제네릭 타입 매개변수 사용
- 그 외 대부분의 경우 → 그냥
int사용
5.4 Integer Operators
var x int = 10
x *= 2 // 복합 대입: += -= *= /= %=
- 산술:
+ - * /, 나머지% - 정수 나눗셈 결과는 정수(0 방향으로 절삭, truncation toward zero) —
-7 / 2→-3 - 0으로 나누면 panic 발생
- 비교:
== != > >= < <= - 비트 연산:
<< >>(시프트),&(AND)|(OR)^(XOR)&^(AND NOT — Go만의 독특한 연산자) - 비트 연산도 복합 대입 가능:
&= |= ^= &^= <<= >>=
5.5 Floating-Point Types
| 타입 | 최대 절댓값 | 최소(0 아닌) 절댓값 |
|---|---|---|
| float32 | 3.40282346638528859811704183484516925440e+38 | 1.401298464324817070923729583289916131280e-45 |
| float64 | 1.797693134862315708145274237317043567981e+308 | 4.940656458412465441765687928682213723651e-324 |
zero value는 0. IEEE 754 표준을 따르며, 넓은 범위 대신 정밀도는 제한적이다.
실무 지침: 특별한 이유 없으면 항상 float64.
- 부동소수점 리터럴의 기본 타입이
float64 float32는 정밀도가 6~7자리뿐이라 오차 누적에 취약- 메모리 크기 차이는 프로파일러로 실제 문제를 확인하기 전까지 신경 쓸 필요 없음
5.6 애초에 float를 써야 하는가
float는 근사치 저장소라는 걸 기억해야 한다. 범위는 넓어도 그 범위 안 모든 실수를 정확히 담을 수는 없어서, 계산할 때마다 반올림 오차가 조금씩 낀다. 그래서 적합한 용도가 정해져 있다 — 그래픽, 통계, 과학 연산처럼 오차 자체가 허용되거나, 오차가 어떻게 누적되는지 다루는 쪽이 이해하고 있는 도메인.
float는 소수 값을 정확히 표현하지 못한다. 돈이나 정확한 소수 표현이 필요한 값에는 절대 사용하지 말 것. (정확한 십진수를 다루는 서드파티 모듈은 이후 “Importing Third-Party Code"에서 다룸.)
5.7 IEEE 754
Go(그리고 대부분의 언어)는 IEEE 754 규격으로 부동소수점을 저장한다. 세부 규칙은 단순하지 않으며 이 글의 범위를 벗어난다. 자세한 내용은 The Floating Point Guide 참고.
%를 제외한 모든 표준 산술·비교 연산자를 float에 쓸 수 있다.
- 0이 아닌 float를 0으로 나누면 →
+Inf/-Inf(부호에 따라) - 0인 float를 0으로 나누면 →
NaN(Not a Number)
==,!=로 float를 비교하지 말 것. 부동소수점은 정확하지 않기 때문에 같아야 할 것 같은 두 값이 실제로는 다를 수 있다. 대신 허용 오차(epsilon)를 정하고 두 값의 차이가 그보다 작은지 확인한다. epsilon 값은 요구되는 정밀도에 따라 다르므로 일률적으로 정하기 어렵다.
6. Complex Types (거의 쓸 일 없음)
Go는 complex64(float32 기반)와 complex128(float64 기반) 두 복소수 타입을 1급으로 지원한다. complex(실수부, 허수부)로 생성하고, real()/imag()로 각 부분을 꺼내며, 리터럴 끝에 i를 붙이면 허수 리터럴이 된다(다섯 번째 리터럴 종류). zero value는 실수부·허수부 모두 0.
x := complex(2.5, 3.1)
fmt.Println(real(x), imag(x))
산술·비교 연산은 float와 동일하게 동작하고(== 비교는 역시 epsilon 권장), math/cmplx 패키지가 추가 함수를 제공한다.
실무에서 자주 쓰이진 않는다 — 행렬 같은 기능이 언어 차원에서 빠져 있어 수치 계산 생태계가 약하다. 만델브로 집합, 이차방정식처럼 복소수가 딱 필요한 경우가 아니면 잘 안 쓰는 기능. Ken Thompson이 재미있어서 넣었다는 일화가 있고, 수치 계산이 본업이면 서드파티 Gonum 패키지를 참고할 만하다.
7. String과 Rune 맛보기
문자열은 다른 언어처럼 내장 타입이고 zero value는 빈 문자열. 유니코드를 그대로 담을 수 있고, ==/!=/비교 연산자/+(연결)를 지원한다. 가장 중요한 특징은 불변(immutable) — 변수 재할당은 되지만 문자열 값 자체는 못 바꾼다.
문자 하나는 rune(= int32의 별칭)으로 표현한다. int32와 타입은 같지만, 문자를 다룬다는 의도를 드러내려면 반드시 rune을 써야 한다.
var myFirstInitial rune = 'J' // good
var myLastInitial int32 = 'B' // bad — 되긴 하지만 의도가 안 보임
8. 명시적 타입 변환
다른 언어들은 서로 다른 숫자 타입 사이에서 자동으로 변환(automatic type promotion)해주는 경우가 많지만, 이 변환 규칙이 은근히 복잡하고 예상 밖의 결과를 낳기 쉽다. Go는 의도를 명확히 드러내는 걸 중시하기 때문에 타입이 다르면 자동 변환을 절대 하지 않는다. 정수와 float는 물론, 크기가 다른 정수 타입끼리도 반드시 명시적으로 변환해야 연산이 가능하다.
var x int = 10
var y float64 = 30.2
var sum1 float64 = float64(x) + y // x를 float64로 변환
var sum2 int = x + int(y) // y를 int로 변환
fmt.Println(sum1, sum2) // 40.2 40
크기가 다른 정수 타입끼리도 마찬가지다.
var x int = 10
var b byte = 100
var sum3 int = x + int(b)
var sum4 byte = byte(x) + b
fmt.Println(sum3, sum4)
이런 엄격함은 불리언에도 그대로 적용된다. 많은 언어에서 0이 아닌 숫자나 빈 문자열이 아닌 값을 true처럼 취급하는 “truthy” 규칙이 있지만, Go에는 그런 게 없다. 어떤 타입도 암묵적으로든 명시적으로든 bool로 변환되지 않는다. 다른 타입을 불리언처럼 쓰고 싶다면 비교 연산자(==, !=, >, <, <=, >=)로 직접 비교해야 한다 — 예: x == 0, s == "".
코드 몇 글자가 늘어나는 대신, 어디서 어떤 타입 변환이 일어나는지 코드만 보고 바로 알 수 있다는 게 이 규칙의 요점.
8.1 리터럴은 타입이 없다
정수 변수끼리는 타입이 다르면 연산이 안 되지만, 정수 리터럴은 float 변수에 바로 대입하거나 float 연산에 섞어 써도 된다.
var x float64 = 10
var y float64 = 200.3 * 5
리터럴은 타입이 정해져 있지 않아서(untyped), 호환되는 타입이면 어디든 그대로 쓸 수 있기 때문이다. 다만 한계는 있다 — 문자열 리터럴을 숫자 변수에, 숫자 리터럴을 문자열 변수에, float 리터럴을 int 변수에 넣는 건 컴파일 에러다. 값의 크기가 대상 타입의 범위를 넘어도(예: byte 변수에 1000 대입) 컴파일 시점에 에러가 난다.
9. var vs :=
Go는 변수 선언 방식이 여러 가지인데, 각 방식이 “이 변수를 어떻게 쓸 것인지"에 대한 의도를 조금씩 다르게 드러낸다.
가장 명시적인 형태는 var + 타입 + 대입:
var x int = 10
우변 값으로 타입을 추론할 수 있으면 타입을 생략해도 된다(정수 리터럴 기본 타입은 int):
var x = 10
반대로 zero value로만 초기화하고 싶으면 대입을 생략:
var x int
같은 타입 여러 개를 한 번에 선언하거나:
var x, y int = 10, 20
var x, y int // 둘 다 zero value
서로 다른 타입을 한 줄에 선언할 수도 있다:
var x, y = 10, "hello"
여러 변수를 선언할 땐 괄호로 묶어 목록 형태로 쓸 수도 있다:
var (
x int
y = 20
z int = 30
d, e = 40, "hello"
f, g string
)
9.1 짧은 선언 :=
함수 안에서는 타입 추론을 쓰는 var를 :=로 대체할 수 있다. 아래 두 줄은 동일하게 동작한다.
var x = 10
x := 10
여러 변수를 한 번에 선언하는 것도 var와 마찬가지로 가능하다.
var x, y = 10, "hello"
x, y := 10, "hello"
:=만의 특징: 좌변에 새 변수가 하나라도 있으면, 이미 존재하는 변수에 값을 재할당하는 것도 같이 할 수 있다.
x := 10
x, y := 30, "hello" // x는 재할당, y는 새로 선언
제약도 있다 — 패키지 레벨(함수 밖)에서는 :=를 쓸 수 없고 반드시 var를 써야 한다.
어떤 스타일을 써야 할까: 의도가 가장 잘 드러나는 쪽을 고르면 된다. 함수 내부에서는 :=가 사실상 표준이고, 함수 밖에서 패키지 레벨 변수를 여러 개 선언할 때만 드물게 var (...) 목록 형태를 쓴다.
9.2 :=를 피해야 하는 경우
- zero value로 초기화할 때:
var x int처럼 써서 “의도적으로 zero value를 쓴다"는 걸 드러낸다. - 리터럴/상수의 기본 타입이 원하는 타입과 다를 때:
x := byte(20)처럼 변환해서 쓸 수도 있지만, 관용적으로는var x byte = 20처럼 타입을 명시하는 쪽을 쓴다. - 기존 변수를 재사용하려는 의도인데 실수로 새 변수가 생기는 걸 막고 싶을 때:
:=는 좌변에 새 변수가 하나만 있어도 통과되기 때문에, 셰도잉(shadowing)이 생기기 쉽다. 헷갈리는 상황이면 새 변수는var로 명시적으로 선언하고, 기존 변수 대입은=로 분리해서 쓴다.
한 줄에 여러 변수를 선언하는 문법(var든 :=든)은 함수가 값을 여러 개 반환하거나 comma-ok 관용구를 쓸 때 정도로만 제한해서 쓰는 게 좋다.
9.3 패키지 블록에는 되도록 선언하지 않는다
함수 밖(패키지 블록)에 변수를 두는 건 되도록 피한다. 값이 바뀌는 패키지 레벨 변수는 어디서 바뀌는지 추적하기 어려워 데이터 흐름을 파악하기 힘들고, 미묘한 버그로 이어지기 쉽다. 패키지 블록에는 사실상 불변인 값만 두는 게 원칙.
9.4 선택 가이드
zero value로 초기화 → var x int
타입 추론 + 함수 내부 → x := 10
타입을 강제로 명시하고 싶을 때 → var x byte = 20
기존 변수 재할당과 새 변수 선언 혼합 → x, y := 30, "hello"
패키지 레벨(함수 밖) 선언 → var만 가능 (:= 불가)
var | := | |
|---|---|---|
| 사용 위치 | 어디서나 (패키지 레벨 포함) | 함수 내부만 |
| 타입 명시 | 가능 | 불가 (추론만) |
| zero value 선언 | var x int | 불가 |
| 기존 변수 재할당 겸용 | 불가 | 가능 (새 변수 하나 이상 필요) |
10. const 사용하기
const는 패키지 레벨이든 함수 내부든 선언할 수 있고, var처럼 괄호로 여러 개를 묶어 선언할 수도 있다.
const x int64 = 10
const (
idKey = "id"
nameKey = "name"
)
const z = 20 * 10
func main() {
const y = "hello"
// x = x + 1 → 컴파일 에러: 상수는 재할당 불가
// y = "bye" → 컴파일 에러
}
한 번 선언한 const는 재할당이 불가능하고, 시도하면 컴파일 에러가 난다.
Go의 const는 생각보다 제한적이다. 컴파일 타임에 값이 확정되는 리터럴에만 이름을 붙이는 용도이지, 다른 언어의 “불변 변수"와는 다르다. const에 담을 수 있는 값은 다음으로 한정된다.
- 숫자 리터럴
true/false- 문자열, 룬
- 내장 함수
complex,real,imag,len,cap의 반환값 - 위 값들과 연산자로 이뤄진 표현식
len, cap은 다음 장에서, const와 함께 쓰이는 iota는 7장(사용자 정의 타입)에서 다룬다.
런타임에 계산된 값은 const로 만들 수 없다. 아래 코드는 컴파일 에러가 난다(x + y는 상수가 아님).
x := 5
y := 10
const z = x + y // 컴파일 에러
같은 이유로 배열·슬라이스·맵·구조체에는 불변 버전이 없고, 구조체 필드를 불변으로 선언하는 방법도 없다. 다만 함수 안에서는 변수가 언제 바뀌는지 코드만 봐도 명확하기 때문에 실제로는 크게 아쉬운 제약이 아니다.
const는 리터럴에 이름을 붙이는 수단일 뿐, Go에는 “이 변수는 불변"이라고 선언하는 방법 자체가 없다.
10.1 타입 있는 상수 vs 타입 없는 상수
const도 리터럴처럼 타입이 없을 수도(untyped), 있을 수도(typed) 있다. 타입 없는 상수는 리터럴과 동일하게 동작해서 문맥에 맞는 타입 어디에나 대입할 수 있고, 타입 있는 상수는 선언된 그 타입에만 바로 대입 가능하다.
const x = 10 // untyped
var y int = x
var z float64 = x
var d byte = x
const typedX int = 10 // typed
var f float64 = typedX // 컴파일 에러
여러 숫자 타입에 두루 쓰일 이름(수학 상수 등)이라면 타입을 지정하지 않는 편이 유연하다. 타입을 강제하고 싶은 경우도 있는데, 대표적인 예가 7장에서 다룰 iota 기반 열거형(enumeration)이다.
| untyped const | typed const | |
|---|---|---|
| 대입 가능 범위 | 호환되는 모든 타입 | 선언된 타입만 |
| 유연성 | 높음 | 낮음 |
| 주 용도 | 여러 타입에 두루 쓰이는 상수 | 타입을 강제해야 하는 경우(예: iota 열거형) |
11. 사용하지 않는 변수
Go는 대규모 협업을 쉽게 하는 걸 목표로 하다 보니, 다른 언어에는 잘 없는 규칙이 있다. 선언한 지역 변수는 반드시 한 번 이상 읽어야 하고, 읽지 않으면 컴파일 에러다.
다만 이 검사는 완벽하지 않다. 값을 한 번이라도 읽기만 하면 통과되기 때문에, 아래처럼 중간에 읽히지 않는 대입이 있어도 컴파일러는 잡아내지 못한다.
func main() {
x := 10 // 이 대입은 읽히지 않음
x = 20
fmt.Println(x)
x = 30 // 이 대입도 읽히지 않음
}
이런 “읽히지 않는 대입"은 go vet도 못 잡고, staticcheck 같은 서드파티 코드 품질 도구로 잡아야 한다.
패키지 레벨 변수는 안 읽혀도 컴파일러가 에러를 내지 않는다. 패키지 레벨 변수 선언을 피해야 할 이유가 하나 더 늘어나는 셈.
12. 사용하지 않는 상수
반대로 const는 안 읽어도 에러가 나지 않는다. 상수는 컴파일 타임에 계산되고 부작용(side effect)이 없어서, 안 쓰이는 상수는 그냥 컴파일된 바이너리에서 빠질 뿐이다.
| 지역 변수 | 패키지 레벨 변수 | 상수 | |
|---|---|---|---|
| 미사용 시 | 컴파일 에러 | 에러 없음 | 에러 없음 |
| 검사 주체 | 컴파일러 | 없음(서드파티 도구 필요) | 해당 없음(바이너리에서 제외) |
13. 변수·상수 이름 짓기
13.1 식별자 규칙은 생각보다 관대하다
Go의 식별자 규칙 자체는 관대하다 — 문자나 언더스코어로 시작하고, 이후 문자·숫자·언더스코어를 쓸 수 있는데, 여기서 “문자"의 범위가 유니코드의 letter/digit 카테고리 전체라 상당히 넓다. 그래서 π나 전각 유니코드 문자, 언더스코어만으로 된 이름도 문법적으로는 유효하다.
π := 3.14
a := "hello" // 전각 유니코드 문자(U+FF41)
__ := "double underscore"
문법적으로는 전부 컴파일된다 — 다만 아래 절에서 보듯 실제로 쓰면 안 되는 이름들이다.
13.2 그래도 안 쓰는 이유 — Look-alike 유니코드
문법적으로 된다고 써도 된다는 뜻은 아니다. 특히 위험한 건 생김새는 같은데 실제로는 다른 유니코드 코드포인트를 쓰는 경우 — 예를 들어 전각 a(U+FF41)와 일반 a(U+0061)는 화면상 거의 구분이 안 되지만 컴파일러 입장에서는 완전히 다른 변수다. 이런 이름은 코드를 읽는 사람도, 타이핑하는 사람도 헷갈리게 만들 뿐이라 관용적인 Go 코드에서는 피한다.
a := "hello" // 전각 a (U+FF41)
a := "goodbye" // 일반 a (U+0061)
fmt.Println(a) // hello
fmt.Println(a) // goodbye — 서로 다른 변수!
13.3 카멜 케이스, 그리고 언더스코어
언더스코어도 문법상 유효하지만 실무에서는 거의 안 쓴다. Go는 여러 단어로 된 식별자에 스네이크 케이스(index_counter) 대신 카멜 케이스(indexCounter)를 관용적으로 사용한다.
indexCounter := 0 // good — 관용적인 Go 스타일
index_counter := 0 // bad — 스네이크 케이스는 안 씀
언더스코어 하나(
_)만 쓰는 건 별도의 특별한 식별자다(5장 함수 파트에서 다룸).
13.4 상수는 대문자 스네이크 케이스로 안 쓴다
다른 언어에서 흔한 “상수는 전부 대문자 스네이크 케이스”(INDEX_COUNTER) 관례도 Go에는 없다. 패키지 레벨 선언은 첫 글자의 대소문자로 패키지 외부 공개 여부를 결정하기 때문(10장 패키지에서 다룸).
const MaxRetries = 3 // 대문자 시작 → 패키지 외부에서도 접근 가능(exported)
const maxRetries = 3 // 소문자 시작 → 같은 패키지 안에서만 접근 가능
INDEX_COUNTER처럼 대문자로 쓰면 오히려 “외부에 공개된 식별자"라는 Go의 의미 규칙과 헷갈릴 수 있다.
13.5 스코프에 따른 이름 길이
스코프가 좁을수록 이름은 짧게. 함수 안, 특히 for 루프에서는 한 글자 이름이 흔하다 — for-range의 키/값에는 k, v, 일반 for 루프의 인덱스에는 i, j. Go는 타입 시스템이 강해서 변수 이름에 타입을 굳이 넣을 필요는 없지만, 관례적으로 타입의 첫 글자를 변수명으로 쓰는 경우도 있다(정수는 i, float는 f 등). 사용자 정의 타입의 리시버 변수명(144p, Methods)도 비슷한 패턴을 따른다.
for i := 0; i < 10; i++ {
fmt.Println(i)
}
m := map[string]int{"a": 1, "b": 2}
for k, v := range m {
fmt.Println(k, v)
}
한 글자짜리 변수를 따라가기 힘들어졌다면, 그건 이름이 문제가 아니라 그 블록이 이미 너무 많은 일을 하고 있다는 신호로 보면 된다.
반면 패키지 블록의 변수·상수는 스코프가 넓으니 더 설명적인 이름을 쓴다(타입은 여전히 이름에 넣지 않음).
14. 연습 문제
- 값이
20인 정수 변수i를 선언하고, float 변수f에 대입한 뒤 둘 다 출력하기 - int와 float 양쪽에 대입 가능한 타입 없는 상수
value를 선언하고, 정수 변수i와 float 변수f에 각각 대입해 출력하기 byte타입b,int32타입smallI,uint64타입bigI에 각 타입의 최댓값을 대입한 뒤 1을 더해서 어떻게 되는지 확인하기(오버플로우 동작 확인용)
풀이
1번
var i int = 20
var f float64 = float64(i) // int → float64 명시적 변환 필요
fmt.Println(i, f)
int와 float64는 서로 다른 타입이라 자동 변환이 안 되므로 float64(i)로 명시적으로 바꿔줘야 한다.
2번
const value = 20 // untyped constant
var i int = value
var f float64 = value
fmt.Println(i, f)
value에 타입을 지정하지 않으면 리터럴처럼 동작해서, 대입되는 변수의 타입에 맞춰 int로도 float64로도 쓰일 수 있다. 타입을 const value int = 20처럼 명시했다면 f에는 대입이 안 되고 컴파일 에러가 난다.
3번
var b byte = 255 // byte 최댓값
var smallI int32 = 2147483647 // int32 최댓값
var bigI uint64 = 18446744073709551615 // uint64 최댓값
b++
smallI++
bigI++
fmt.Println(b, smallI, bigI)
결과:
0 -2147483648 0
Go는 정수 오버플로우를 감지해서 에러를 내지 않는다. 대신 값의 비트 표현이 그대로 순환(wrap around)한다.
byte(0~255) 최댓값255에 1을 더하면 → 비트가 전부 넘쳐서0int32는 부호 있는 타입이라 최댓값에 1을 더하면 최상위 비트가 부호 비트로 뒤집혀 최솟값(-2147483648)이 된다uint64도byte와 같은 원리로 최댓값 + 1 →0
0으로 나누기는 런타임 panic을 일으키지만, 오버플로우는 panic 없이 조용히 값을 순환시킨다는 점이 이 문제의 핵심 포인트.
15. 마치며
핵심 요약
1. 타입이 다르면 자동 변환 없음 — 항상 명시적으로 변환하라
2. 값이 안 바뀐다 → var x int
타입을 강제하고 싶다 → var x byte = 20
함수 안에서 빠르게 → x := 10
컴파일 타임 리터럴에 이름만 붙인다 → const
3. 패키지 블록에는 되도록 선언하지 않는다
사전 선언 타입, 리터럴, 변수 선언 방식(var/:=), 상수(const), 명시적 타입 변환, 네이밍 규칙까지 Go의 타입·선언 시스템 전반을 훑었다. 다음 장에서는 배열·슬라이스·맵·구조체 같은 복합 타입과, 문자열·룬이 문자 인코딩과 어떻게 얽히는지를 다룬다.
