Go to Rust: #2. 기본 문법과 타입 시스템
이 글은 Claude Opus 4.5 을 이용해 초안이 작성되었으며, 이후 퇴고를 거쳤습니다.
이 섹션에서는 Rust의 기본 문법을 Go와 비교하며 학습합니다. Go 개발자에게 익숙한 개념들이 Rust에서 어떻게 표현되는지, 그리고 Rust만의 독특한 특징은 무엇인지 알아봅니다.
2.1 변수와 가변성#
let vs var: 불변이 기본인 이유#
Go에서는 변수가 기본적으로 가변(mutable)입니다:
// Go: 기본이 가변
x := 10
x = 20 // OK
Rust에서는 불변(immutable)이 기본입니다:
// Rust: 기본이 불변
let x = 10;
x = 20; // 컴파일 에러!
왜 불변이 기본일까요?
- 안전성: 의도치 않은 수정 방지
- 동시성: 불변 데이터는 스레드 안전
- 추론 용이: 값이 변하지 않으면 코드 이해가 쉬움
- 최적화: 컴파일러가 더 공격적으로 최적화 가능
mut 키워드: 명시적 가변성#
변경이 필요한 변수는 mut을 명시합니다:
let mut x = 10;
x = 20; // OK
let mut name = String::from("Alice");
name.push_str(" Smith"); // OK
Go와 비교하면:
// Go: 가변성이 암묵적
x := 10
x = 20
name := "Alice"
name = name + " Smith"
// Rust: 가변성이 명시적
let mut x = 10;
x = 20;
let mut name = String::from("Alice");
name.push_str(" Smith");
섀도잉(Shadowing)#
Go에는 없는 개념입니다. 같은 이름으로 새 변수를 선언하면 이전 변수를 “가림(shadow)“니다:
let x = 5;
let x = x + 1; // 새로운 x, 값은 6
let x = x * 2; // 또 새로운 x, 값은 12
// 타입도 변경 가능!
let spaces = " "; // &str
let spaces = spaces.len(); // usize
섀도잉 vs mut:
// 섀도잉: 새 변수 생성, 타입 변경 가능
let x = "hello";
let x = x.len(); // OK, x는 이제 usize
// mut: 같은 변수, 타입 변경 불가
let mut y = "hello";
y = y.len(); // 컴파일 에러! 타입이 다름
섀도잉은 Go에서 하던 이런 패턴을 대체합니다:
// Go: 다른 이름 사용해야 함
spacesStr := " "
spacesLen := len(spacesStr)
// Rust: 같은 이름 재사용 가능
let spaces = " ";
let spaces = spaces.len();
상수(const)와 정적 변수(static)#
const: 컴파일 타임 상수
const MAX_POINTS: u32 = 100_000;
const PI: f64 = 3.14159;
const GREETING: &str = "Hello";
Go와 비교:
// Go
const MaxPoints = 100_000
const Pi = 3.14159
const Greeting = "Hello"
차이점:
- Rust는 타입 명시 필수
- Rust는 표현식 계산 가능 (컴파일 타임에)
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
static: 전역 변수 (런타임)
static LANGUAGE: &str = "Rust";
static mut COUNTER: u32 = 0; // 가변 static은 unsafe
fn main() {
println!("{}", LANGUAGE);
// 가변 static 접근은 unsafe 블록 필요
unsafe {
COUNTER += 1;
}
}
Go에서는:
// Go: 패키지 레벨 변수
var Language = "Go"
var counter = 0 // 동시성 문제 잠재적
const vs static:
| 특성 | const | static |
|---|---|---|
| 메모리 | 인라인됨 | 고정 주소 |
| 가변성 | 항상 불변 | static mut 가능 |
| 용도 | 컴파일 타임 값 | 전역 상태, FFI |
2.2 기본 데이터 타입#
스칼라 타입#
Rust는 Go보다 더 명시적인 정수 타입을 제공합니다:
정수 타입:
| 크기 | 부호 있음 | 부호 없음 | Go 대응 |
|---|---|---|---|
| 8-bit | i8 |
u8 |
int8, uint8 |
| 16-bit | i16 |
u16 |
int16, uint16 |
| 32-bit | i32 |
u32 |
int32, uint32 |
| 64-bit | i64 |
u64 |
int64, uint64 |
| 128-bit | i128 |
u128 |
없음 |
| arch | isize |
usize |
int, uint |
let a: i32 = 42;
let b: u64 = 100;
let c: isize = -10; // 포인터 크기 (32 또는 64비트)
// 타입 접미사
let x = 42i64;
let y = 100u8;
// 밑줄로 가독성 향상
let million = 1_000_000;
let binary = 0b1111_0000;
let hex = 0xff_ff;
부동소수점:
let x: f64 = 3.14; // 기본
let y: f32 = 2.5;
// Go와 동일
// Go: float64, float32
불리언:
let t: bool = true;
let f: bool = false;
// Go와 동일
// Go: bool
문자:
let c: char = 'A';
let emoji: char = '😀';
let korean: char = '가';
// char는 4바이트 유니코드 스칼라 값
// Go의 rune과 유사 (둘 다 4바이트)
타입 추론과 명시적 타입 지정#
Rust는 Go처럼 타입 추론을 지원하지만, 더 강력합니다:
// 타입 추론
let x = 42; // i32 (기본)
let y = 3.14; // f64 (기본)
let z = true; // bool
// 문맥에 따른 추론
let v: Vec<i32> = Vec::new(); // 타입 명시
let v = Vec::<i32>::new(); // 터보피시 문법
let v: Vec<_> = vec![1, 2, 3]; // 부분 추론
// Go 비교
// var x = 42 // int
// var y = 3.14 // float64
타입 변환: as 키워드#
Go와 달리 Rust는 암묵적 타입 변환을 하지 않습니다:
let x: i32 = 42;
let y: i64 = x; // 컴파일 에러!
let y: i64 = x as i64; // OK
let a: f64 = 3.14;
let b: i32 = a as i32; // 3 (소수점 버림)
// Go
// var x int32 = 42
// var y int64 = int64(x) // 명시적 변환 필요 (동일)
안전한 변환 vs 위험한 변환:
// 안전: 작은 타입 → 큰 타입
let small: u8 = 100;
let big: u32 = small as u32; // 항상 안전
// 위험: 큰 타입 → 작은 타입
let big: u32 = 300;
let small: u8 = big as u8; // 44 (오버플로우!)
// 안전한 변환을 원하면 TryFrom 사용
use std::convert::TryFrom;
let result = u8::try_from(big); // Result<u8, Error>
리터럴 표기법#
// 정수 리터럴
let decimal = 98_222;
let hex = 0xff;
let octal = 0o77;
let binary = 0b1111_0000;
let byte = b'A'; // u8
// 부동소수점 리터럴
let float = 3.14;
let scientific = 2.5e10;
// 문자열 리터럴
let s = "Hello";
let raw = r"C:\Users\name"; // raw string
let raw_with_quotes = r#"He said "Hello""#;
// 바이트 문자열
let bytes = b"Hello"; // &[u8; 5]
2.3 복합 타입#
튜플(Tuple)#
Go에는 없는 타입입니다. 여러 값을 하나로 묶습니다:
// 튜플 생성
let tup: (i32, f64, bool) = (500, 6.4, true);
// 구조 분해
let (x, y, z) = tup;
println!("y = {}", y);
// 인덱스 접근
let five_hundred = tup.0;
let six_point_four = tup.1;
let is_true = tup.2;
Go의 다중 반환값과 비교:
// Go: 다중 반환값
func getPoint() (int, int) {
return 10, 20
}
x, y := getPoint()
// Rust: 튜플 반환
fn get_point() -> (i32, i32) {
(10, 20)
}
let (x, y) = get_point();
// 또는
let point = get_point();
let x = point.0;
let y = point.1;
유닛 타입 ():
// 빈 튜플 = 유닛 타입
// Go의 void와 유사하지만, 실제 값임
fn no_return() {
// 암묵적으로 () 반환
}
fn explicit_unit() -> () {
()
}
배열(Array)#
고정 크기, 스택 할당:
// 배열 선언
let arr: [i32; 5] = [1, 2, 3, 4, 5];
// 같은 값으로 초기화
let zeros: [i32; 5] = [0; 5]; // [0, 0, 0, 0, 0]
// 접근
let first = arr[0];
let second = arr[1];
// 길이
let len = arr.len(); // 5
Go 배열과 비교:
// Go
var arr [5]int = [5]int{1, 2, 3, 4, 5}
zeros := [5]int{} // [0, 0, 0, 0, 0]
범위 검사:
let arr = [1, 2, 3];
let index = 10;
// 컴파일 타임에 알 수 있으면 에러
// let x = arr[10]; // 컴파일 에러
// 런타임에만 알 수 있으면 패닉
// let x = arr[index]; // 런타임 패닉
// 안전한 접근
let x = arr.get(index); // Option<&i32>
match x {
Some(value) => println!("{}", value),
None => println!("Index out of bounds"),
}
슬라이스(Slice) 참조#
슬라이스는 배열의 일부를 참조:
let arr = [1, 2, 3, 4, 5];
// 슬라이스 생성 (참조!)
let slice: &[i32] = &arr[1..4]; // [2, 3, 4]
let all: &[i32] = &arr[..]; // 전체
let from_start: &[i32] = &arr[..3]; // [1, 2, 3]
let to_end: &[i32] = &arr[2..]; // [3, 4, 5]
Go slice와의 결정적 차이:
// Go: slice는 동적 배열 (소유권 있음)
slice := []int{1, 2, 3, 4, 5}
slice = append(slice, 6) // 크기 변경 가능
// 부분 슬라이스도 가능
sub := slice[1:4]
// Rust: slice는 참조 (소유권 없음)
let arr = [1, 2, 3, 4, 5];
let slice = &arr[1..4]; // 빌림!
// 동적 배열은 Vec<T>
let mut vec = vec![1, 2, 3, 4, 5];
vec.push(6); // 크기 변경 가능
// Vec의 슬라이스
let slice: &[i32] = &vec[1..4];
| 개념 | Go | Rust |
|---|---|---|
| 동적 배열 | []T (slice) |
Vec<T> |
| 참조/뷰 | slice[a:b] |
&[T], &slice[a..b] |
| 소유권 | 슬라이스가 소유 | Vec이 소유, 슬라이스는 빌림 |
2.4 함수#
함수 선언 문법#
fn function_name(param1: Type1, param2: Type2) -> ReturnType {
// 함수 본문
}
Go와 비교:
// Go
func functionName(param1 Type1, param2 Type2) ReturnType {
// 함수 본문
}
// Rust
fn add(a: i32, b: i32) -> i32 {
a + b // 세미콜론 없음 = 반환값
}
// 또는 명시적 return
fn add_explicit(a: i32, b: i32) -> i32 {
return a + b;
}
표현식(Expression) vs 문장(Statement)#
Rust의 핵심 개념 중 하나:
- 문장(Statement): 동작을 수행하고 값을 반환하지 않음
- 표현식(Expression): 값을 반환함
fn main() {
// 문장: let 바인딩
let x = 5; // 이것은 문장, 값을 반환하지 않음
// Go/C와 달리 이건 안 됨!
// let x = (let y = 6); // 에러!
// 블록은 표현식
let y = {
let inner = 3;
inner + 1 // 세미콜론 없음 = 이 값이 블록의 값
}; // y = 4
// 세미콜론이 있으면 문장이 되어 () 반환
let z = {
let inner = 3;
inner + 1; // 세미콜론 있음!
}; // z = ()
}
세미콜론의 의미:
fn returns_five() -> i32 {
5 // 표현식, 5를 반환
}
fn returns_unit() -> () {
5; // 문장, () 반환
}
// 흔한 실수
fn oops() -> i32 {
5; // 컴파일 에러! expected i32, found ()
}
다중 반환: 튜플 활용#
Go처럼 다중 값을 반환하려면 튜플을 사용합니다:
// Go
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
result, err := divide(10, 2)
// Rust (Result 타입 사용 - 나중에 자세히)
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
return Err(String::from("division by zero"));
}
Ok(a / b)
}
// 또는 튜플로 직접
fn divide_tuple(a: i32, b: i32) -> (i32, bool) {
if b == 0 {
return (0, false);
}
(a / b, true)
}
let (result, ok) = divide_tuple(10, 2);
매개변수 전달#
// 값으로 전달 (소유권 이동 또는 복사)
fn take_ownership(s: String) {
println!("{}", s);
} // s가 드롭됨
// 참조로 전달 (빌림)
fn borrow(s: &String) {
println!("{}", s);
} // 빌림 끝, 원본 유지
// 가변 참조로 전달
fn modify(s: &mut String) {
s.push_str(" modified");
}
2.5 제어 흐름#
if 표현식#
Go와 달리 Rust의 if는 표현식입니다:
// 기본 if
let number = 5;
if number < 10 {
println!("less than 10");
} else if number < 20 {
println!("less than 20");
} else {
println!("20 or more");
}
// 조건에 괄호 불필요 (Go와 동일)
// 중괄호는 필수 (Go와 동일)
if를 표현식으로 사용:
let condition = true;
// 삼항 연산자 대신 if 표현식
let number = if condition { 5 } else { 6 };
// Go에서는 불가능
// x := if condition { 5 } else { 6 } // 문법 에러
// Go에서는 이렇게 해야 함
// var number int
// if condition {
// number = 5
// } else {
// number = 6
// }
if let: 패턴 매칭과 결합
let some_value: Option<i32> = Some(42);
// 전통적인 match
match some_value {
Some(x) => println!("Got: {}", x),
None => (),
}
// if let: 하나의 패턴만 관심 있을 때
if let Some(x) = some_value {
println!("Got: {}", x);
}
// else와 결합
if let Some(x) = some_value {
println!("Got: {}", x);
} else {
println!("Got nothing");
}
반복문#
loop: 무한 루프
// 무한 루프
loop {
println!("Forever!");
break; // 탈출
}
// loop는 값을 반환할 수 있음!
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // 20 반환
}
};
// Go에서는 불가능
// for {
// // 무한 루프
// }
while: 조건부 루프
let mut number = 3;
while number != 0 {
println!("{}!", number);
number -= 1;
}
// Go
// for number != 0 {
// fmt.Println(number)
// number--
// }
for: 이터레이터 기반 루프
// 범위
for i in 0..5 {
println!("{}", i); // 0, 1, 2, 3, 4
}
for i in 0..=5 {
println!("{}", i); // 0, 1, 2, 3, 4, 5 (inclusive)
}
// 역순
for i in (0..5).rev() {
println!("{}", i); // 4, 3, 2, 1, 0
}
// 컬렉션 순회
let arr = [10, 20, 30];
for element in arr {
println!("{}", element);
}
// 인덱스와 함께
for (index, element) in arr.iter().enumerate() {
println!("[{}] = {}", index, element);
}
Go의 for와 비교:
// Go: 모든 루프가 for
for i := 0; i < 5; i++ {
fmt.Println(i)
}
for index, element := range arr {
fmt.Printf("[%d] = %d\n", index, element)
}
// 무한 루프
for {
// ...
}
// while 스타일
for condition {
// ...
}
// Rust: 용도별로 분리
// C 스타일 for는 없음!
// 범위 기반
for i in 0..5 {
println!("{}", i);
}
// while
while condition {
// ...
}
// 무한 루프
loop {
// ...
}
break, continue, 레이블#
// break와 continue
for i in 0..10 {
if i == 3 {
continue; // 다음 반복으로
}
if i == 7 {
break; // 루프 종료
}
println!("{}", i);
}
// 레이블로 중첩 루프 제어
'outer: for i in 0..5 {
for j in 0..5 {
if i == 2 && j == 2 {
break 'outer; // 바깥 루프 종료
}
println!("({}, {})", i, j);
}
}
// Go도 레이블 지원
// outer:
// for i := 0; i < 5; i++ {
// for j := 0; j < 5; j++ {
// if i == 2 && j == 2 {
// break outer
// }
// }
// }
while let#
if let처럼 패턴이 매치되는 동안 반복:
let mut stack = vec![1, 2, 3];
// pop()은 Option<T> 반환
while let Some(top) = stack.pop() {
println!("{}", top); // 3, 2, 1
}
// 동등한 코드
loop {
match stack.pop() {
Some(top) => println!("{}", top),
None => break,
}
}
2.6 주석과 문서화#
일반 주석#
// 한 줄 주석
/*
* 여러 줄 주석
* Go와 동일
*/
let x = 5; // 인라인 주석
문서 주석#
Rust는 문서화를 언어 차원에서 지원합니다:
/// 두 숫자를 더합니다.
///
/// # Arguments
///
/// * `a` - 첫 번째 숫자
/// * `b` - 두 번째 숫자
///
/// # Returns
///
/// 두 숫자의 합
///
/// # Examples
///
/// ```
/// let result = mylib::add(2, 3);
/// assert_eq!(result, 5);
/// ```
///
/// # Panics
///
/// 이 함수는 패닉하지 않습니다.
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
//! 모듈이나 크레이트 수준 문서
//!
//! 이 크레이트는 수학 연산을 제공합니다.
Go의 godoc과 비교:
// Go: 함수 바로 위에 주석
// Add adds two numbers and returns the result.
func Add(a, b int) int {
return a + b
}
// Rust: Markdown 지원, 구조화된 섹션
/// Adds two numbers and returns the result.
///
/// # Examples
///
/// ```
/// assert_eq!(add(2, 3), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
문서 테스트(Doc Tests)#
Rust의 강력한 기능: 문서의 예제 코드가 실제로 테스트됩니다:
/// 문자열을 대문자로 변환합니다.
///
/// # Examples
///
/// ```
/// let s = mylib::to_upper("hello");
/// assert_eq!(s, "HELLO");
/// ```
///
/// 빈 문자열도 처리합니다:
///
/// ```
/// let s = mylib::to_upper("");
/// assert_eq!(s, "");
/// ```
pub fn to_upper(s: &str) -> String {
s.to_uppercase()
}
# 문서 테스트 실행
cargo test --doc
# 모든 테스트 (유닛 + 통합 + 문서)
cargo test
Go에서는 Example 함수를 별도로 작성해야 합니다:
// Go: example_test.go
func ExampleAdd() {
fmt.Println(Add(2, 3))
// Output: 5
}
문서 생성#
# 문서 생성 및 브라우저에서 열기
cargo doc --open
# 의존성 문서도 포함
cargo doc --open --document-private-items
2.7 요약: Go와 Rust 기본 문법 비교#
변수 선언#
// Go
var x int = 10
x := 10
var x int // 0으로 초기화
// Rust
let x: i32 = 10;
let x = 10;
let x: i32; // 초기화 없이 선언 (사용 전 초기화 필수)
let mut x = 10; // 가변
타입#
| 개념 | Go | Rust |
|---|---|---|
| 부호 있는 정수 | int8~int64, int |
i8~i128, isize |
| 부호 없는 정수 | uint8~uint64, uint |
u8~u128, usize |
| 부동소수점 | float32, float64 |
f32, f64 |
| 불리언 | bool |
bool |
| 문자 | rune (int32) |
char (4바이트) |
| 문자열 | string |
String, &str |
| 배열 | [N]T |
[T; N] |
| 슬라이스 | []T |
&[T], Vec<T> |
| 맵 | map[K]V |
HashMap<K, V> |
함수#
// Go
func add(a, b int) int {
return a + b
}
// Rust
fn add(a: i32, b: i32) -> i32 {
a + b
}
제어 흐름#
// Go: 모든 루프가 for
for i := 0; i < 10; i++ {}
for condition {}
for {}
for i, v := range slice {}
// Rust: 용도별 키워드
for i in 0..10 {}
while condition {}
loop {}
for (i, v) in slice.iter().enumerate() {}
연습 문제#
-
변수 선언: 다음 Go 코드를 Rust로 변환하세요:
x := 10 x = 20 name := "Alice" age := 30 -
타입 변환:
i32값을i64로 변환하고,f64값을i32로 변환하세요. -
튜플: 이름(String), 나이(u32), 활성상태(bool)를 담는 튜플을 만들고 구조 분해하세요.
-
if 표현식: 숫자가 양수면 “positive”, 음수면 “negative”, 0이면 “zero"를 반환하는 함수를 작성하세요.
if를 표현식으로 사용하세요. -
루프: 1부터 100까지의 합을
loop,while,for각각으로 계산하세요. -
문서 주석: 두 문자열을 결합하는 함수에 문서 주석과 예제를 작성하세요.