고 기초 #2 변수, 타입, 상수

6 분 소요

#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
var name string = "커티스"
var age int = 30
var ok bool = true

가장 명시적인 형태. 타입을 적습니다.

1.5) var — 타입 추론 #

var + 추론
var name = "커티스"   // string으로 추론
var age = 30          // int로 추론

초깃값에서 타입을 추론할 수 있으면 타입 명시 생략 가능.

2) := — 단축 선언 #

함수 안에서 가장 흔한 형태.

:=
name := "커티스"
age := 30
ok := true

var 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 블록
var (
	name string = "커티스"
	age  int    = 30
	ok   bool   = true
)

여러 var를 묶어 정리할 때. 패키지 수준 변수에서 자주 만나는 모양입니다.

영(zero) 값 #

선언만 하고 초깃값을 안 주면 — Go가 **영값(zero value)**으로 초기화해 줍니다.

zero values
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 기본
const Pi = 3.14159
const MaxRetries = 3
const AppName = "MyApp"

var와 비슷하지만 — 컴파일 시점 상수라서 런타임에 바뀌지 않고, 함수 호출 결과 같은 값은 const로 둘 수 없습니다.

const의 한계
const Now = time.Now()   // ✗ time.Now()는 런타임 호출

타입 있는 const vs 없는 const #

typed vs untyped 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 비슷한 역할입니다.

iota 기본
const (
	Red   = iota   // 0
	Green          // 1
	Blue           // 2
)

iota는 const 블록 안에서 0부터 시작해 한 줄씩 1 씩 증가합니다. 같은 표현을 반복할 필요 없이 자동.

iota 활용 #

다양한 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 — 의미 있는 타입 만들기 #

기본 타입을 그대로 쓰지 않고 자기 이름의 타입을 만들 수 있습니다.

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 자주 쓰는 것
fmt.Println("줄 끝에 자동 newline")
fmt.Print("줄 끝 newline 없음")

fmt.Printf("이름: %s, 나이: %d\n", name, age)

s := fmt.Sprintf("결과: %d", 42)   // 문자열로 만들기

자주 쓰는 포맷 verb:

verb의미
%v기본 표현 (어떤 타입이든)
%+v구조체 필드 이름까지
%d10진 정수
%s문자열
%q따옴표 친 문자열
%tbool
%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를 돌려줍니다.

byte vs rune
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) :== 헷갈리기 #

새 변수 vs 재할당
x := 10        // 새 변수 (선언 + 할당)
x = 20         // 재할당
y := 30        // 새 변수
// y := 40      ✗ y는 이미 선언됨 (단, 다중 선언 중 하나라도 새 변수 있으면 OK)

여러 변수 중 하나라도 새 것이면 :=가 통합니다.

다중 선언의 := 동작
x := 10
x, y := 20, 30   // x 재할당, y 새 변수 — OK

2) 정수 나눗셈은 절단 #

정수 / 정수 = 정수
result := 10 / 3        // 3 (소수점 절단)
result2 := 10.0 / 3.0   // 3.333...
result3 := float64(10) / 3   // 3.333...

다른 언어처럼 자동 변환되지 않습니다. 부동소수 결과를 원하면 명시적 변환.

마무리 #

이번 글에서 정리한 내용:

  • 정적 타입 — 모든 변수의 타입이 컴파일 시점에 정해짐
  • var:= 두 가지 선언 방식
  • 함수 안에서는 :=가 거의 표준
  • 영값 — 선언만 해도 의미 있는 초깃값 자동
  • 자동 타입 변환 거의 없음 — 명시적 변환 필요
  • 문자열 ↔ 숫자는 strconv
  • const와 untyped vs typed
  • iota로 자동 증가 / 비트 플래그 / 단위 표현
  • named type으로 의미 있는 타입 만들기
  • 문자열은 byte 시퀀스, rune 단위 순회는 for range

다음 글(#3 제어 흐름)에서는 if/for/switch와 그 안에서의 모던 패턴들을 다룹니다. Go에는 while이 없고 for가 모든 반복을 합니다.

X