이 글은 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;  // 컴파일 에러!

왜 불변이 기본일까요?

  1. 안전성: 의도치 않은 수정 방지
  2. 동시성: 불변 데이터는 스레드 안전
  3. 추론 용이: 값이 변하지 않으면 코드 이해가 쉬움
  4. 최적화: 컴파일러가 더 공격적으로 최적화 가능

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() {}

연습 문제#

  1. 변수 선언: 다음 Go 코드를 Rust로 변환하세요:

    x := 10
    x = 20
    name := "Alice"
    age := 30
    
  2. 타입 변환: i32 값을 i64로 변환하고, f64 값을 i32로 변환하세요.

  3. 튜플: 이름(String), 나이(u32), 활성상태(bool)를 담는 튜플을 만들고 구조 분해하세요.

  4. if 표현식: 숫자가 양수면 “positive”, 음수면 “negative”, 0이면 “zero"를 반환하는 함수를 작성하세요. if를 표현식으로 사용하세요.

  5. 루프: 1부터 100까지의 합을 loop, while, for 각각으로 계산하세요.

  6. 문서 주석: 두 문자열을 결합하는 함수에 문서 주석과 예제를 작성하세요.