📚 GO 기본문법 - 변수와 함수

📋 목차


1. 변수 선언

💡 변수 선언의 4가지 방법

1. 명시적 선언 (var 키워드)

package main
 
import "fmt"
 
func main() {
    // 타입과 함께 선언
    var name string = "Go"
    var age int = 13
    var isActive bool = true
    
    // 기본값으로 초기화 (zero value)
    var count int      // 0
    var message string // ""
    var flag bool      // false
    
    fmt.Printf("Name: %s, Age: %d, Active: %t\n", name, age, isActive)
    fmt.Printf("Count: %d, Message: '%s', Flag: %t\n", count, message, flag)
}

2. 타입 추론 (var 키워드 + 추론)

func variableInference() {
    // Go가 타입을 자동으로 추론
    var language = "Golang"     // string으로 추론
    var version = 1.21          // float64로 추론
    var downloads = 1000000     // int로 추론
    
    fmt.Printf("Language: %s (Type: %T)\n", language, language)
    fmt.Printf("Version: %.1f (Type: %T)\n", version, version)
    fmt.Printf("Downloads: %d (Type: %T)\n", downloads, downloads)
}

3. 짧은 선언 (Short Declaration)

func shortDeclaration() {
    // 함수 내부에서만 사용 가능!
    nickname := "Gopher"
    year := 2025
    pi := 3.14159
    
    // 한 줄에 여러 변수 선언
    x, y := 10, 20
    firstName, lastName := "John", "Doe"
    
    fmt.Printf("Nickname: %s, Year: %d, Pi: %.2f\n", nickname, year, pi)
    fmt.Printf("Coordinates: (%d, %d)\n", x, y)
    fmt.Printf("Full Name: %s %s\n", firstName, lastName)
}

4. 그룹 선언

var (
    // 패키지 레벨 변수들
    appName    = "MyGoApp"
    version    = "1.0.0"
    maxUsers   = 1000
    isDebug    = false
)
 
func groupDeclaration() {
    // 함수 내부 그룹 선언
    var (
        width  = 1920
        height = 1080
        fps    = 60
    )
    
    fmt.Printf("App: %s v%s, Max Users: %d\n", appName, version, maxUsers)
    fmt.Printf("Resolution: %dx%d@%dfps\n", width, height, fps)
}

🔄 변수 재할당

func reassignment() {
    // 변수 값 변경
    var score int = 0
    score = 100  // 재할당
    score += 25  // score = score + 25
    
    // 짧은 선언으로 새 변수 추가
    name := "Alice"
    name, age := "Bob", 30  // name은 재할당, age는 새 선언
    
    fmt.Printf("Score: %d\n", score)
    fmt.Printf("Name: %s, Age: %d\n", name, age)
}

📊 기본 데이터 타입

func dataTypes() {
    // 정수형
    var smallInt int8 = 127           // -128 ~ 127
    var mediumInt int16 = 32767       // -32768 ~ 32767
    var normalInt int32 = 2147483647  // -2^31 ~ 2^31-1
    var bigInt int64 = 9223372036854775807 // -2^63 ~ 2^63-1
    var autoInt int = 42              // 플랫폼에 따라 32bit 또는 64bit
    
    // 부호 없는 정수형
    var uSmall uint8 = 255            // 0 ~ 255 (byte와 같음)
    var uMedium uint16 = 65535        // 0 ~ 65535
    var uNormal uint32 = 4294967295   // 0 ~ 2^32-1
    var uBig uint64 = 18446744073709551615 // 0 ~ 2^64-1
    
    // 실수형
    var precision float32 = 3.14159   // 32bit 부동소수점
    var doublePrecision float64 = 3.141592653589793 // 64bit 부동소수점
    
    // 문자열과 문자
    var text string = "Hello, Go!"   // UTF-8 문자열
    var char rune = 'G'              // Unicode 코드 포인트 (int32 별칭)
    var byteVal byte = 65            // uint8 별칭 (ASCII 'A')
    
    // 불린
    var isReady bool = true          // true 또는 false
    
    // 복소수 (잘 안 씀)
    var complexNum complex64 = 1 + 2i
    var bigComplexNum complex128 = 1 + 2i
    
    fmt.Printf("Integer types: %d, %d, %d, %d, %d\n", 
        smallInt, mediumInt, normalInt, bigInt, autoInt)
    fmt.Printf("Float types: %.2f, %.6f\n", precision, doublePrecision)
    fmt.Printf("String: %s, Rune: %c, Byte: %c\n", text, char, byteVal)
    fmt.Printf("Boolean: %t\n", isReady)
}

2. 함수 정의

🔧 기본 함수 문법

// 기본 함수 형태
func functionName(parameter1 type1, parameter2 type2) returnType {
    // 함수 본문
    return value
}

💫 다양한 함수 예제

1. 매개변수와 반환값이 있는 함수

// 두 수를 더하는 함수
func add(a int, b int) int {
    return a + b
}
 
// 같은 타입 매개변수는 축약 가능
func multiply(a, b int) int {
    return a * b
}
 
// 문자열 조작 함수
func greet(name string) string {
    return "Hello, " + name + "!"
}
 
func basicFunctions() {
    result1 := add(10, 20)
    result2 := multiply(5, 6)
    message := greet("Gopher")
    
    fmt.Printf("10 + 20 = %d\n", result1)
    fmt.Printf("5 × 6 = %d\n", result2)
    fmt.Printf("%s\n", message)
}

2. 다중 반환값 (Go의 특징!)

// 나눗셈과 나머지를 동시에 반환
func divideAndMod(a, b int) (int, int) {
    quotient := a / b
    remainder := a % b
    return quotient, remainder
}
 
// 에러와 함께 반환 (일반적인 패턴)
func divide(a, b int) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return float64(a) / float64(b), nil
}
 
// 이름있는 반환값 (named return values)
func getCircleStats(radius float64) (area, circumference float64) {
    area = 3.14159 * radius * radius
    circumference = 2 * 3.14159 * radius
    return // 자동으로 area, circumference 반환
}
 
func multipleReturns() {
    // 다중 반환값 받기
    quotient, remainder := divideAndMod(17, 5)
    fmt.Printf("17 ÷ 5 = %d remainder %d\n", quotient, remainder)
    
    // 에러 처리 패턴
    result, err := divide(10, 2)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
    } else {
        fmt.Printf("10 ÷ 2 = %.2f\n", result)
    }
    
    // 이름있는 반환값
    area, circum := getCircleStats(5.0)
    fmt.Printf("Circle (r=5): Area=%.2f, Circumference=%.2f\n", area, circum)
}

3. 가변 인자 함수 (Variadic Functions)

// 가변 개수의 정수를 받아서 합계 계산
func sum(numbers ...int) int {
    total := 0
    for _, num := range numbers {
        total += num
    }
    return total
}
 
// 가변 인자 + 다른 매개변수
func formatMessage(prefix string, messages ...string) string {
    result := prefix + ": "
    for i, msg := range messages {
        if i > 0 {
            result += ", "
        }
        result += msg
    }
    return result
}
 
func variadicFunctions() {
    // 다양한 개수의 인자 전달
    fmt.Printf("Sum of 1,2,3: %d\n", sum(1, 2, 3))
    fmt.Printf("Sum of 1,2,3,4,5: %d\n", sum(1, 2, 3, 4, 5))
    
    // 슬라이스를 가변 인자로 전달
    numbers := []int{10, 20, 30}
    fmt.Printf("Sum of slice: %d\n", sum(numbers...))
    
    // 메시지 포맷팅
    message := formatMessage("Alert", "Server down", "Database error", "High CPU")
    fmt.Println(message)
}

4. 함수도 값이다! (First-class functions)

// 함수 타입 정의
type Operation func(int, int) int
 
// 함수를 매개변수로 받는 함수
func calculate(a, b int, op Operation) int {
    return op(a, b)
}
 
// 함수를 반환하는 함수
func getOperation(operator string) Operation {
    switch operator {
    case "+":
        return add
    case "*":
        return multiply
    default:
        return func(a, b int) int { return 0 }
    }
}
 
func functionsAsValues() {
    // 함수를 변수에 할당
    var myFunc Operation = add
    result := myFunc(10, 20)
    fmt.Printf("Function variable: %d\n", result)
    
    // 함수를 매개변수로 전달
    result2 := calculate(15, 3, multiply)
    fmt.Printf("Function parameter: %d\n", result2)
    
    // 익명 함수 사용
    subtract := func(a, b int) int {
        return a - b
    }
    result3 := calculate(20, 8, subtract)
    fmt.Printf("Anonymous function: %d\n", result3)
    
    // 함수를 반환받아 사용
    addFunc := getOperation("+")
    result4 := addFunc(100, 200)
    fmt.Printf("Returned function: %d\n", result4)
}

3. 에러 처리

🚨 Go의 에러 처리 철학

Go의 에러 처리

Go는 예외(exception) 대신 명시적인 에러 반환을 사용합니다. 에러는 값이며, 반드시 확인해야 합니다.

import (
    "errors"
    "fmt"
    "strconv"
)
 
// 기본 에러 처리 패턴
func parseAge(s string) (int, error) {
    age, err := strconv.Atoi(s)
    if err != nil {
        return 0, fmt.Errorf("invalid age format: %s", s)
    }
    
    if age < 0 || age > 150 {
        return 0, errors.New("age must be between 0 and 150")
    }
    
    return age, nil
}
 
// 커스텀 에러 타입
type ValidationError struct {
    Field   string
    Value   string
    Message string
}
 
func (e ValidationError) Error() string {
    return fmt.Sprintf("validation error in field '%s': %s (value: %s)", 
        e.Field, e.Message, e.Value)
}
 
// 커스텀 에러를 반환하는 함수
func validateEmail(email string) error {
    if email == "" {
        return ValidationError{
            Field:   "email",
            Value:   email,
            Message: "email cannot be empty",
        }
    }
    
    if !strings.Contains(email, "@") {
        return ValidationError{
            Field:   "email",
            Value:   email,
            Message: "email must contain @ symbol",
        }
    }
    
    return nil
}
 
func errorHandling() {
    // 기본 에러 처리
    age1, err := parseAge("25")
    if err != nil {
        fmt.Printf("Error parsing age: %v\n", err)
    } else {
        fmt.Printf("Valid age: %d\n", age1)
    }
    
    // 잘못된 입력 테스트
    age2, err := parseAge("abc")
    if err != nil {
        fmt.Printf("Error parsing age: %v\n", err)
    }
    
    // 커스텀 에러 처리
    if err := validateEmail(""); err != nil {
        fmt.Printf("Email validation error: %v\n", err)
        
        // 에러 타입 확인
        if validErr, ok := err.(ValidationError); ok {
            fmt.Printf("Field: %s, Value: %s\n", validErr.Field, validErr.Value)
        }
    }
    
    if err := validateEmail("test@example.com"); err != nil {
        fmt.Printf("Email validation error: %v\n", err)
    } else {
        fmt.Println("Email is valid!")
    }
}

🛡️ 에러 처리 베스트 프랙티스

// 1. 에러 체크 생략하지 말기
func badExample() {
    data, _ := ioutil.ReadFile("config.json")  // 나쁜 예: 에러 무시
    // 파일이 없어도 프로그램이 계속 실행됨
}
 
func goodExample() {
    data, err := ioutil.ReadFile("config.json")
    if err != nil {
        log.Fatalf("Failed to read config file: %v", err)
        return
    }
    // 안전하게 data 사용
}
 
// 2. 에러 래핑 (Go 1.13+)
func processFile(filename string) error {
    data, err := ioutil.ReadFile(filename)
    if err != nil {
        return fmt.Errorf("failed to read file %s: %w", filename, err)
    }
    
    if err := validateData(data); err != nil {
        return fmt.Errorf("validation failed for file %s: %w", filename, err)
    }
    
    return nil
}
 
// 3. 에러 체인 확인
func checkErrorChain() {
    err := processFile("nonexistent.txt")
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        
        // 원본 에러 확인
        if errors.Is(err, os.ErrNotExist) {
            fmt.Println("File does not exist")
        }
    }
}

4. 포인터 기초

🎯 포인터란?

포인터

포인터는 메모리 주소를 저장하는 변수입니다. Go의 포인터는 C보다 안전하며, 포인터 산술 연산은 지원하지 않습니다.

func pointerBasics() {
    // 기본 변수
    x := 42
    
    // 포인터 선언 및 초기화
    var p *int    // int 타입을 가리키는 포인터
    p = &x        // x의 주소를 p에 저장
    
    fmt.Printf("x의 값: %d\n", x)
    fmt.Printf("x의 주소: %p\n", &x)
    fmt.Printf("p의 값 (주소): %p\n", p)
    fmt.Printf("p가 가리키는 값: %d\n", *p)  // 역참조
    
    // 포인터를 통한 값 변경
    *p = 100
    fmt.Printf("포인터로 변경 후 x의 값: %d\n", x)
}

🔄 함수와 포인터

// 값에 의한 호출 (Call by Value)
func doubleValue(x int) int {
    x = x * 2
    return x  // 원본은 변경되지 않음
}
 
// 포인터에 의한 호출 (Call by Pointer)
func doublePointer(x *int) {
    *x = *x * 2  // 원본 값이 변경됨
}
 
// 구조체와 포인터
type Person struct {
    Name string
    Age  int
}
 
func (p Person) growUpValue() {
    p.Age++  // 복사본의 나이만 증가
}
 
func (p *Person) growUpPointer() {
    p.Age++  // 원본의 나이가 증가
}
 
func pointerFunctions() {
    // 값 전달 vs 포인터 전달
    num := 10
    
    doubled := doubleValue(num)
    fmt.Printf("After doubleValue - num: %d, returned: %d\n", num, doubled)
    
    doublePointer(&num)
    fmt.Printf("After doublePointer - num: %d\n", num)
    
    // 구조체와 포인터
    person := Person{Name: "Alice", Age: 25}
    
    person.growUpValue()
    fmt.Printf("After growUpValue: %+v\n", person)
    
    person.growUpPointer()
    fmt.Printf("After growUpPointer: %+v\n", person)
}

🆕 새로운 메모리 할당

func memoryAllocation() {
    // new() 함수: 영점으로 초기화된 메모리 할당
    p1 := new(int)        // *int 타입, 0으로 초기화
    *p1 = 42
    fmt.Printf("new()로 할당: %d\n", *p1)
    
    // &를 사용한 주소 획득
    x := 100
    p2 := &x
    fmt.Printf("&로 주소 획득: %d\n", *p2)
    
    // make()는 슬라이스, 맵, 채널용
    slice := make([]int, 5)  // 길이 5인 int 슬라이스
    slice[0] = 10
    fmt.Printf("make()로 슬라이스: %v\n", slice)
}

🎯 실습 예제

📝 연습 문제 1: 계산기 함수

package main
 
import (
    "fmt"
    "errors"
)
 
// 사칙연산 계산기
func calculator(a, b float64, operator string) (float64, error) {
    switch operator {
    case "+":
        return a + b, nil
    case "-":
        return a - b, nil
    case "*":
        return a * b, nil
    case "/":
        if b == 0 {
            return 0, errors.New("division by zero")
        }
        return a / b, nil
    default:
        return 0, fmt.Errorf("unknown operator: %s", operator)
    }
}
 
func calculatorTest() {
    operations := []struct {
        a, b     float64
        operator string
    }{
        {10, 5, "+"},
        {10, 5, "-"},
        {10, 5, "*"},
        {10, 5, "/"},
        {10, 0, "/"},  // 에러 케이스
        {10, 5, "%"},  // 에러 케이스
    }
    
    for _, op := range operations {
        result, err := calculator(op.a, op.b, op.operator)
        if err != nil {
            fmt.Printf("%.1f %s %.1f = Error: %v\n", 
                op.a, op.operator, op.b, err)
        } else {
            fmt.Printf("%.1f %s %.1f = %.2f\n", 
                op.a, op.operator, op.b, result)
        }
    }
}

📝 연습 문제 2: 문자열 처리 함수

import "strings"
 
// 문자열 통계 정보
type StringStats struct {
    Length    int
    Words     int
    Vowels    int
    Uppercase int
    Lowercase int
}
 
func analyzeString(text string) StringStats {
    stats := StringStats{
        Length: len(text),
        Words:  len(strings.Fields(text)),
    }
    
    vowels := "aeiouAEIOU"
    
    for _, char := range text {
        if strings.ContainsRune(vowels, char) {
            stats.Vowels++
        }
        
        if char >= 'A' && char <= 'Z' {
            stats.Uppercase++
        } else if char >= 'a' && char <= 'z' {
            stats.Lowercase++
        }
    }
    
    return stats
}
 
func stringAnalysisTest() {
    texts := []string{
        "Hello, Go!",
        "The Quick Brown Fox Jumps Over The Lazy Dog",
        "golang is AWESOME",
    }
    
    for _, text := range texts {
        stats := analyzeString(text)
        fmt.Printf("\nText: %s\n", text)
        fmt.Printf("  Length: %d characters\n", stats.Length)
        fmt.Printf("  Words: %d\n", stats.Words)
        fmt.Printf("  Vowels: %d\n", stats.Vowels)
        fmt.Printf("  Uppercase: %d\n", stats.Uppercase)
        fmt.Printf("  Lowercase: %d\n", stats.Lowercase)
    }
}

📝 연습 문제 3: 포인터를 사용한 스왑 함수

// 두 값을 교환하는 함수
func swap(a, b *int) {
    temp := *a
    *a = *b
    *b = temp
}
 
// 문자열 스왑
func swapStrings(s1, s2 *string) {
    *s1, *s2 = *s2, *s1  // Go의 다중 할당 사용
}
 
func swapTest() {
    // 정수 스왑
    x, y := 10, 20
    fmt.Printf("Before swap: x=%d, y=%d\n", x, y)
    swap(&x, &y)
    fmt.Printf("After swap: x=%d, y=%d\n", x, y)
    
    // 문자열 스왑
    name1, name2 := "Alice", "Bob"
    fmt.Printf("Before swap: name1=%s, name2=%s\n", name1, name2)
    swapStrings(&name1, &name2)
    fmt.Printf("After swap: name1=%s, name2=%s\n", name1, name2)
}

✅ 체크리스트

변수와 타입

  • 4가지 변수 선언 방법 이해
  • 기본 데이터 타입 파악
  • 타입 추론 원리 이해
  • Zero values 개념 파악

함수

  • 기본 함수 문법 이해
  • 다중 반환값 활용법
  • 가변 인자 함수 사용법
  • 함수를 값으로 다루기

에러 처리

  • Go의 에러 처리 철학 이해
  • 기본 에러 처리 패턴
  • 커스텀 에러 타입 생성
  • 에러 래핑과 체인

포인터

  • 포인터 기본 개념
  • 값 전달 vs 포인터 전달
  • 메모리 할당 방법
  • 포인터 안전성 이해

다음 단계

기본 문법을 익혔다면 GO 구조체와 인터페이스로 넘어가세요!