고 기초 #2 변수, 타입, 상수
#1 시작과 첫 프로그램에서 환경을 만들었으니 — 이번엔 언어 자체를 다룹니다. Go의 기본 타입과 변수 선언 방식, 상수까지 정리합니다.
기본 타입 #
Go는 정적 타입 언어입니다. 컴파일 시점에 모든 변수의 타입이 정해져 있습니다.
| 종류 | 타입 |
|---|---|
| 정수 | int, int8, int16, int32, int64, uint, uint8 (= byte), … |
| 부동소수 | float32, float64 |
| 복소수 | complex64, complex128 |
| 불리언 | bool |
| 문자열 | string |
| 룬 (유니코드 코드 포인트) | rune (= int32) |
가장 자주 쓰는 건 int, string, bool, float64. 비트 너비가 정해진 타입(int32, int64 등)은 정확한 크기가 필요할 때만 씁니다.
int의 크기
#
int는 플랫폼에 따라 32비트 또는 64비트입니다. 64비트 OS에서는 보통 int64와 같습니다. 정확한 크기가 중요할 때는 int32 / int64를 명시.
변수 선언 — 두 가지 방식 #
1) var — 명시적 선언
#
var name string = "커티스"
var age int = 30
var ok bool = true가장 명시적인 형태. 타입을 적습니다.
1.5) var — 타입 추론
#
var name = "커티스" // string으로 추론
var age = 30 // int로 추론초깃값에서 타입을 추론할 수 있으면 타입 명시 생략 가능.
2) := — 단축 선언
#
함수 안에서 가장 흔한 형태.
name := "커티스"
age := 30
ok := truevar x = ...와 의미가 거의 같지만 함수 안에서만 쓸 수 있습니다. **함수 안의 새 변수에는 거의 항상 :=**가 표준 컨벤션입니다.
함수 밖에서는 := 불가
#
package main
var globalName = "커티스" // OK
// globalName2 := "다른" ✗ 함수 밖에선 := 불가
func main() {
localName := "지역" // OK
}여러 변수 한 번에 #
var a, b, c int = 1, 2, 3
var x, y = 1, "hi" // 다른 타입 OK
i, j := 10, 20
i, j = j, i // swap:= 구문에서 swap 같은 패턴이 자연스럽습니다.
var 블록
#
var (
name string = "커티스"
age int = 30
ok bool = true
)여러 var를 묶어 정리할 때. 패키지 수준 변수에서 자주 만나는 모양입니다.
영(zero) 값 #
선언만 하고 초깃값을 안 주면 — Go가 **영값(zero value)**으로 초기화해 줍니다.
var i int // 0
var f float64 // 0.0
var b bool // false
var s string // "" (빈 문자열)
var p *int // nil (포인터의 영값)다른 언어의 null/undefined와 다른 점 — Go에서는 모든 변수가 항상 의미 있는 값을 가집니다. “선언만 하고 안 쓴 것” 같은 상태가 없습니다.
타입 변환 — 명시적 #
Go는 자동 타입 변환을 거의 하지 않습니다. 다른 타입 사이에서는 명시적 변환이 필요합니다.
i := 42
f := float64(i) // int → float64
ui := uint(f) // float64 → uint
// i + f ✗ mismatched types
i + int(f) // OK처음에는 답답해도, 의도하지 않은 정밀도 손실을 막아주는 디자인입니다.
문자열 ↔ 숫자 #
이 경우는 변환 함수가 따로 있습니다.
import "strconv"
s := strconv.Itoa(42) // int → string ("42")
n, err := strconv.Atoi("42") // string → int (에러 가능)
f, err := strconv.ParseFloat("3.14", 64)에러 반환은 #4 함수, 다중 반환, error 타입에서 자세히.
상수 — const
#
값이 컴파일 시점에 정해지고 바뀌지 않으면 const.
const Pi = 3.14159
const MaxRetries = 3
const AppName = "MyApp"var와 비슷하지만 — 컴파일 시점 상수라서 런타임에 바뀌지 않고, 함수 호출 결과 같은 값은 const로 둘 수 없습니다.
const Now = time.Now() // ✗ time.Now()는 런타임 호출타입 있는 const vs 없는 const #
const Pi = 3.14159 // untyped — 문맥에 따라 타입 결정
const PiTyped float32 = 3.14159 // typed — float32로 고정
var f float64 = Pi // OK — Pi가 untyped
var f32 float32 = Pi // OK — Pi가 untyped
var f32 float32 = PiTyped // OK
var f64 float64 = PiTyped // ✗ float32 → float64 명시 변환 필요타입 없는(untyped) const는 더 유연합니다. 특별한 이유가 없으면 untyped로 두는 게 보통입니다.
iota — 자동 증가 const
#
같은 const 그룹 안에서 자동으로 증가하는 값을 만들 수 있습니다. 다른 언어의 enum 비슷한 역할입니다.
const (
Red = iota // 0
Green // 1
Blue // 2
)iota는 const 블록 안에서 0부터 시작해 한 줄씩 1 씩 증가합니다. 같은 표현을 반복할 필요 없이 자동.
iota 활용 #
// 상수 생략
const (
A = iota // 0
B // 1
C // 2
_ // 3 (건너뛰기)
E // 4
)
// 비트 플래그
const (
Read = 1 << iota // 1 (1 << 0)
Write // 2 (1 << 1)
Execute // 4 (1 << 2)
)
// 단위
const (
_ = iota // 무시 (iota = 0)
KB = 1 << (10 * iota) // 1024
MB // 1024 * 1024
GB // 1024^3
)이 패턴들이 라이브러리 코드에서 자주 등장합니다. 비트 플래그가 특히 자주 쓰입니다.
Named type — 의미 있는 타입 만들기 #
기본 타입을 그대로 쓰지 않고 자기 이름의 타입을 만들 수 있습니다.
type UserID string
type Email string
func sendMessage(id UserID, to Email) {
// ...
}
var u UserID = "u1"
var e Email = "me@example.com"
sendMessage(u, e) // OK
// sendMessage(e, u) ✗ UserID와 Email은 다른 타입같은 string 인데도 — UserID와 Email은 호환되지 않습니다. 의미를 타입에 부여해 잘못된 호출을 컴파일 단계에서 막을 수 있습니다. 타입스크립트의 branded type (TS 심화 #5)과 비슷한 효과입니다.
기본 출력 / 입력 — fmt
#
자주 쓰는 fmt 패키지 함수들.
fmt.Println("줄 끝에 자동 newline")
fmt.Print("줄 끝 newline 없음")
fmt.Printf("이름: %s, 나이: %d\n", name, age)
s := fmt.Sprintf("결과: %d", 42) // 문자열로 만들기자주 쓰는 포맷 verb:
| verb | 의미 |
|---|---|
%v | 기본 표현 (어떤 타입이든) |
%+v | 구조체 필드 이름까지 |
%d | 10진 정수 |
%s | 문자열 |
%q | 따옴표 친 문자열 |
%t | bool |
%f | 부동소수 |
%T | 타입 이름 |
fmt.Printf("%d %s %t\n", 42, "hi", true) // 42 hi true
fmt.Printf("%v %T\n", 3.14, 3.14) // 3.14 float64
fmt.Printf("%q\n", "hello") // "hello"문자열의 두 가지 얼굴 #
Go의 문자열은 불변 바이트 시퀀스 입니다. 인덱스 접근은 byte를 돌려줍니다.
s := "안녕"
fmt.Println(len(s)) // 6 — UTF-8 바이트 수
fmt.Println(s[0]) // 236 — 첫 바이트 (숫자)
// 문자(rune) 단위 순회
for i, r := range s {
fmt.Printf("%d %c\n", i, r)
}
// 0 안
// 3 녕for range는 룬(유니코드 코드 포인트) 단위로 순회합니다. 한글/이모지를 다룰 때는 byte 인덱스가 아니라 rune으로 다뤄야 합니다.
자주 만나는 함정들 #
1) :=와 = 헷갈리기
#
x := 10 // 새 변수 (선언 + 할당)
x = 20 // 재할당
y := 30 // 새 변수
// y := 40 ✗ y는 이미 선언됨 (단, 다중 선언 중 하나라도 새 변수 있으면 OK)여러 변수 중 하나라도 새 것이면 :=가 통합니다.
x := 10
x, y := 20, 30 // x 재할당, y 새 변수 — OK2) 정수 나눗셈은 절단 #
result := 10 / 3 // 3 (소수점 절단)
result2 := 10.0 / 3.0 // 3.333...
result3 := float64(10) / 3 // 3.333...다른 언어처럼 자동 변환되지 않습니다. 부동소수 결과를 원하면 명시적 변환.
마무리 #
이번 글에서 정리한 내용:
- 정적 타입 — 모든 변수의 타입이 컴파일 시점에 정해짐
var와:=두 가지 선언 방식- 함수 안에서는
:=가 거의 표준 - 영값 — 선언만 해도 의미 있는 초깃값 자동
- 자동 타입 변환 거의 없음 — 명시적 변환 필요
- 문자열 ↔ 숫자는
strconv const와 untyped vs typediota로 자동 증가 / 비트 플래그 / 단위 표현- named type으로 의미 있는 타입 만들기
- 문자열은 byte 시퀀스, rune 단위 순회는
for range
다음 글(#3 제어 흐름)에서는 if/for/switch와 그 안에서의 모던 패턴들을 다룹니다. Go에는 while이 없고 for가 모든 반복을 합니다.