- Authors

- Name
- Youngju Kim
- @fjvbn20031
소개
Go(Golang)는 Google이 2009년 발표한 정적 타입, 컴파일형 언어입니다. 단순한 문법, 빠른 컴파일, 강력한 동시성 지원, 풍부한 표준 라이브러리가 특징입니다. 클라우드 인프라, 마이크로서비스, CLI 도구 개발에 특히 강력합니다.
이 가이드는 Go의 핵심 문법을 처음부터 끝까지 체계적으로 정리합니다.
1. Go 언어 기초 문법
1.1 변수 선언
Go에서 변수를 선언하는 방법은 여러 가지입니다.
package main
import "fmt"
func main() {
// var 키워드로 선언 (타입 명시)
var name string = "Go"
var age int = 15
// 타입 추론 (var)
var pi = 3.14159
// 단축 선언 (:=) - 함수 내부에서만 사용 가능
greeting := "Hello, Go!"
count := 42
// 복수 변수 선언
var x, y int = 10, 20
a, b := 100, 200
// 제로값: 선언 시 초기화하지 않으면 타입의 기본값
var zeroInt int // 0
var zeroStr string // ""
var zeroBool bool // false
fmt.Println(name, age, pi, greeting, count)
fmt.Println(x, y, a, b)
fmt.Println(zeroInt, zeroStr, zeroBool)
}
1.2 상수와 iota
package main
import "fmt"
// 단순 상수
const Pi = 3.14159
const MaxSize = 1024
// 상수 블록
const (
StatusOK = 200
StatusNotFound = 404
StatusError = 500
)
// iota: 열거형 상수 자동 증가
type Direction int
const (
North Direction = iota // 0
East // 1
South // 2
West // 3
)
type ByteSize float64
const (
_ = iota // blank identifier로 0 건너뜀
KB ByteSize = 1 << (10 * iota) // 1 << 10 = 1024
MB // 1 << 20
GB // 1 << 30
)
func main() {
fmt.Println(North, East, South, West) // 0 1 2 3
fmt.Printf("KB=%.0f, MB=%.0f, GB=%.0f\n", float64(KB), float64(MB), float64(GB))
}
1.3 기본 타입
package main
import "fmt"
func main() {
// 정수 타입
var i8 int8 = 127
var i16 int16 = 32767
var i32 int32 = 2147483647
var i64 int64 = 9223372036854775807
var ui uint = 42
var ui64 uint64 = 18446744073709551615
// 플랫폼 의존 (64비트 시스템에서 int64)
var n int = 100
// 부동소수점
var f32 float32 = 3.14
var f64 float64 = 3.141592653589793
// 복소수
var c128 complex128 = 3 + 4i
// 문자열 (UTF-8 인코딩, 불변)
s := "Hello, 세계"
fmt.Println(len(s)) // 바이트 수
fmt.Println([]rune(s)) // 룬(유니코드 코드포인트) 슬라이스
// byte (uint8의 별칭)
var b byte = 'A'
// rune (int32의 별칭, 유니코드 코드포인트)
var r rune = '한'
// bool
var flag bool = true
fmt.Println(i8, i16, i32, i64, ui, ui64, n)
fmt.Println(f32, f64, c128)
fmt.Println(b, r, flag)
}
1.4 배열과 슬라이스
package main
import "fmt"
func main() {
// 배열: 고정 크기, 값 타입
var arr [5]int
arr[0] = 10
arr2 := [3]string{"Go", "Python", "Rust"}
arr3 := [...]int{1, 2, 3, 4, 5} // 컴파일러가 크기 추론
fmt.Println(arr, arr2, arr3)
// 슬라이스: 동적 크기, 참조 타입
s1 := []int{1, 2, 3}
s2 := make([]int, 3) // len=3, cap=3
s3 := make([]int, 3, 10) // len=3, cap=10
fmt.Println(len(s1), cap(s1)) // 3, 3
fmt.Println(len(s2), cap(s2)) // 3, 3
fmt.Println(len(s3), cap(s3)) // 3, 10
// append: 용량 초과 시 새 배열 할당
s1 = append(s1, 4, 5)
fmt.Println(s1) // [1 2 3 4 5]
// 슬라이싱
s4 := s1[1:3] // [2 3] (원본과 메모리 공유)
s5 := s1[2:] // [3 4 5]
s6 := s1[:2] // [1 2]
fmt.Println(s4, s5, s6)
// copy: 독립적인 복사본 생성
dst := make([]int, len(s1))
n := copy(dst, s1)
fmt.Printf("복사된 원소 수: %d, dst: %v\n", n, dst)
// 2차원 슬라이스
matrix := make([][]int, 3)
for i := range matrix {
matrix[i] = make([]int, 3)
for j := range matrix[i] {
matrix[i][j] = i*3 + j
}
}
fmt.Println(matrix)
}
1.5 맵
package main
import "fmt"
func main() {
// 맵 선언과 초기화
m1 := map[string]int{
"apple": 5,
"banana": 3,
"cherry": 8,
}
m2 := make(map[string][]string)
// 값 추가/수정
m1["date"] = 12
m2["fruits"] = []string{"apple", "banana"}
// 값 읽기
val := m1["apple"]
fmt.Println(val) // 5
// comma-ok 패턴: 키 존재 여부 확인
val2, ok := m1["mango"]
if !ok {
fmt.Println("mango not found, zero value:", val2)
}
// 삭제
delete(m1, "cherry")
fmt.Println(m1)
// 순회 (순서 보장 안 됨)
for key, value := range m1 {
fmt.Printf("%s: %d\n", key, value)
}
// nil 맵에 쓰기는 패닉 발생
var m3 map[string]int
// m3["key"] = 1 // panic: assignment to entry in nil map
_ = m3
}
1.6 포인터
package main
import "fmt"
func increment(n *int) {
*n++ // 역참조
}
func newInt(val int) *int {
return &val // 로컬 변수의 주소 반환 가능 (힙 이동)
}
func main() {
x := 42
p := &x // x의 주소
fmt.Println(p) // 메모리 주소 출력 (예: 0xc0000b4010)
fmt.Println(*p) // 42 (역참조)
*p = 100
fmt.Println(x) // 100
increment(&x)
fmt.Println(x) // 101
ptr := newInt(200)
fmt.Println(*ptr) // 200
// nil 포인터
var nilPtr *int
fmt.Println(nilPtr) // <nil>
// fmt.Println(*nilPtr) // panic: nil pointer dereference
}
1.7 제어문
package main
import "fmt"
func main() {
// for: Go의 유일한 반복문
for i := 0; i < 5; i++ {
fmt.Print(i, " ")
}
fmt.Println()
// while 스타일
n := 1
for n < 100 {
n *= 2
}
fmt.Println(n)
// range
nums := []int{10, 20, 30, 40}
for idx, val := range nums {
fmt.Printf("[%d]=%d ", idx, val)
}
fmt.Println()
// 인덱스만 사용
for i := range nums {
fmt.Print(nums[i], " ")
}
fmt.Println()
// switch (break 없이도 fallthrough 안 됨)
day := "Monday"
switch day {
case "Saturday", "Sunday":
fmt.Println("주말")
case "Monday":
fmt.Println("월요일")
default:
fmt.Println("평일")
}
// 조건 없는 switch (if-else 대체)
score := 85
switch {
case score >= 90:
fmt.Println("A")
case score >= 80:
fmt.Println("B")
case score >= 70:
fmt.Println("C")
default:
fmt.Println("F")
}
// defer: 함수 종료 시 실행 (LIFO 순서)
fmt.Println("시작")
defer fmt.Println("첫 번째 defer")
defer fmt.Println("두 번째 defer")
fmt.Println("끝")
// 출력: 시작 -> 끝 -> 두 번째 defer -> 첫 번째 defer
}
2. 함수와 메서드
2.1 다중 반환값과 Named Return
package main
import (
"errors"
"fmt"
)
// 다중 반환값
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Named return values
func minMax(nums []int) (min, max int) {
if len(nums) == 0 {
return // 제로값 반환
}
min, max = nums[0], nums[0]
for _, n := range nums[1:] {
if n < min {
min = n
}
if n > max {
max = n
}
}
return // naked return
}
// Variadic functions (가변 인수)
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
func main() {
result, err := divide(10, 3)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Printf("%.4f\n", result) // 3.3333
}
_, err = divide(5, 0)
if err != nil {
fmt.Println("Error:", err) // division by zero
}
min, max := minMax([]int{3, 1, 4, 1, 5, 9, 2, 6})
fmt.Println(min, max) // 1 9
fmt.Println(sum(1, 2, 3, 4, 5)) // 15
// 슬라이스를 variadic 인수로 펼치기
nums := []int{10, 20, 30}
fmt.Println(sum(nums...)) // 60
}
2.2 일급 함수와 클로저
package main
import "fmt"
// 함수를 인수로 받기
func apply(nums []int, fn func(int) int) []int {
result := make([]int, len(nums))
for i, n := range nums {
result[i] = fn(n)
}
return result
}
// 함수를 반환하기
func multiplier(factor int) func(int) int {
return func(n int) int {
return n * factor
}
}
// 클로저: 외부 변수를 캡처
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
double := func(n int) int { return n * 2 }
nums := []int{1, 2, 3, 4, 5}
fmt.Println(apply(nums, double)) // [2 4 6 8 10]
fmt.Println(apply(nums, func(n int) int { return n * n })) // [1 4 9 16 25]
triple := multiplier(3)
fmt.Println(triple(7)) // 21
c1 := counter()
c2 := counter()
fmt.Println(c1(), c1(), c1()) // 1 2 3
fmt.Println(c2(), c2()) // 1 2 (독립적인 상태)
}
2.3 메서드 (Value Receiver vs Pointer Receiver)
package main
import (
"fmt"
"math"
)
type Circle struct {
Radius float64
}
// Value receiver: 복사본에 적용 (원본 변경 불가)
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}
// Pointer receiver: 원본 변경 가능
func (c *Circle) Scale(factor float64) {
c.Radius *= factor
}
type Rectangle struct {
Width, Height float64
}
func (r *Rectangle) Area() float64 {
return r.Width * r.Height
}
// init() 함수: 패키지 초기화 시 자동 실행
func init() {
fmt.Println("패키지 초기화")
}
func main() {
c := Circle{Radius: 5}
fmt.Printf("넓이: %.2f\n", c.Area())
fmt.Printf("둘레: %.2f\n", c.Perimeter())
c.Scale(2)
fmt.Printf("스케일 후 반지름: %.1f\n", c.Radius) // 10.0
// 포인터 메서드를 값에서 호출 가능 (자동 주소 취득)
c2 := Circle{Radius: 3}
(&c2).Scale(1.5) // 자동으로 &c2로 변환
}
3. 구조체와 인터페이스
3.1 구조체와 임베딩
package main
import "fmt"
type Animal struct {
Name string
Age int
}
func (a Animal) Speak() string {
return a.Name + " makes a sound"
}
// 임베딩: 상속이 아닌 구성(Composition)
type Dog struct {
Animal // 임베딩 (익명 필드)
Breed string
}
func (d Dog) Speak() string {
return d.Name + " says: Woof!"
}
type ServiceDog struct {
Dog
ServiceType string
}
func main() {
d := Dog{
Animal: Animal{Name: "Buddy", Age: 3},
Breed: "Labrador",
}
// 임베딩된 필드 직접 접근
fmt.Println(d.Name) // d.Animal.Name과 동일
fmt.Println(d.Age)
fmt.Println(d.Breed)
fmt.Println(d.Speak()) // Dog의 Speak() 호출
fmt.Println(d.Animal.Speak()) // Animal의 Speak() 명시적 호출
sd := ServiceDog{
Dog: d,
ServiceType: "Guide",
}
fmt.Println(sd.Name) // sd.Dog.Animal.Name
fmt.Println(sd.ServiceType)
}
3.2 인터페이스
package main
import (
"fmt"
"math"
)
// 인터페이스 정의
type Shape interface {
Area() float64
Perimeter() float64
}
type Stringer interface {
String() string
}
type Circle struct{ Radius float64 }
type Rectangle struct{ Width, Height float64 }
func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }
func (c Circle) String() string { return fmt.Sprintf("Circle(r=%.1f)", c.Radius) }
func (r Rectangle) Area() float64 { return r.Width * r.Height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) }
func (r Rectangle) String() string {
return fmt.Sprintf("Rect(%.1fx%.1f)", r.Width, r.Height)
}
// 인터페이스를 인수로 받는 함수
func printShapeInfo(s Shape) {
fmt.Printf("넓이: %.2f, 둘레: %.2f\n", s.Area(), s.Perimeter())
}
// 빈 인터페이스 (모든 타입)
func printAny(v interface{}) {
fmt.Printf("타입: %T, 값: %v\n", v, v)
}
// 타입 어서션
func describe(i interface{}) {
if s, ok := i.(string); ok {
fmt.Printf("문자열: %q\n", s)
return
}
if n, ok := i.(int); ok {
fmt.Printf("정수: %d\n", n)
return
}
fmt.Printf("알 수 없는 타입: %T\n", i)
}
// 타입 스위치
func typeSwitch(v interface{}) string {
switch x := v.(type) {
case int:
return fmt.Sprintf("int: %d", x)
case string:
return fmt.Sprintf("string: %q", x)
case bool:
return fmt.Sprintf("bool: %v", x)
case []int:
return fmt.Sprintf("[]int with %d elements", len(x))
default:
return fmt.Sprintf("unknown type: %T", x)
}
}
func main() {
shapes := []Shape{
Circle{Radius: 5},
Rectangle{Width: 4, Height: 6},
}
for _, s := range shapes {
printShapeInfo(s)
if str, ok := s.(Stringer); ok {
fmt.Println(str.String())
}
}
printAny(42)
printAny("hello")
printAny([]int{1, 2, 3})
describe("Go언어")
describe(2024)
fmt.Println(typeSwitch(100))
fmt.Println(typeSwitch("world"))
fmt.Println(typeSwitch([]int{1, 2}))
}
4. 에러 처리
4.1 에러 타입과 커스텀 에러
package main
import (
"errors"
"fmt"
)
// 센티널 에러 (패키지 수준 에러 변수)
var (
ErrNotFound = errors.New("not found")
ErrPermission = errors.New("permission denied")
)
// 커스텀 에러 타입
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error: %s - %s", e.Field, e.Message)
}
type DatabaseError struct {
Code int
Message string
Err error // wrapped error
}
func (e *DatabaseError) Error() string {
return fmt.Sprintf("database error [%d]: %s", e.Code, e.Message)
}
func (e *DatabaseError) Unwrap() error {
return e.Err
}
// fmt.Errorf로 에러 래핑
func findUser(id int) error {
if id <= 0 {
return fmt.Errorf("findUser: invalid id %d: %w", id, ErrNotFound)
}
return nil
}
func getUser(id int) error {
if err := findUser(id); err != nil {
return fmt.Errorf("getUser: %w", err)
}
return nil
}
func main() {
// errors.Is: 에러 체인에서 특정 에러 확인
err := getUser(-1)
if errors.Is(err, ErrNotFound) {
fmt.Println("not found 에러:", err)
}
// errors.As: 에러 체인에서 특정 타입 추출
dbErr := &DatabaseError{
Code: 500,
Message: "connection failed",
Err: ErrPermission,
}
wrapped := fmt.Errorf("service error: %w", dbErr)
var de *DatabaseError
if errors.As(wrapped, &de) {
fmt.Printf("DB 에러 코드: %d\n", de.Code)
}
if errors.Is(wrapped, ErrPermission) {
fmt.Println("권한 에러가 체인에 포함됨")
}
// 커스텀 에러 생성
valErr := &ValidationError{Field: "email", Message: "invalid format"}
fmt.Println(valErr)
}
4.2 panic과 recover
package main
import "fmt"
// recover로 panic 복구
func safeDiv(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
}
}()
return a / b, nil
}
func mustPositive(n int) int {
if n <= 0 {
panic(fmt.Sprintf("값은 양수여야 합니다: %d", n))
}
return n
}
func main() {
result, err := safeDiv(10, 2)
fmt.Printf("10/2 = %d, err = %v\n", result, err)
result, err = safeDiv(10, 0)
fmt.Printf("10/0 = %d, err = %v\n", result, err)
// 정상적인 panic 사용: 프로그래밍 오류나 복구 불가능한 상황
defer func() {
if r := recover(); r != nil {
fmt.Println("패닉 복구:", r)
}
}()
mustPositive(-5)
}
5. 고루틴과 채널 (동시성)
5.1 고루틴과 WaitGroup
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d 시작\n", id)
time.Sleep(time.Millisecond * 100)
fmt.Printf("Worker %d 완료\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait()
fmt.Println("모든 워커 완료")
}
5.2 채널
package main
import (
"fmt"
"time"
)
func producer(ch chan<- int, count int) {
for i := 0; i < count; i++ {
ch <- i
time.Sleep(time.Millisecond * 10)
}
close(ch)
}
func main() {
// 언버퍼드 채널 (동기)
ch1 := make(chan int)
go func() { ch1 <- 42 }()
fmt.Println(<-ch1) // 42
// 버퍼드 채널 (비동기, 버퍼 차기 전까지 블록 안 됨)
ch2 := make(chan string, 3)
ch2 <- "first"
ch2 <- "second"
ch2 <- "third"
fmt.Println(<-ch2) // first
// 채널 순회
numbers := make(chan int, 10)
go producer(numbers, 5)
for n := range numbers {
fmt.Print(n, " ")
}
fmt.Println()
// select 문: 여러 채널 동시 처리
ch3 := make(chan string, 1)
ch4 := make(chan string, 1)
go func() {
time.Sleep(time.Millisecond * 50)
ch3 <- "채널3"
}()
go func() {
time.Sleep(time.Millisecond * 30)
ch4 <- "채널4"
}()
for i := 0; i < 2; i++ {
select {
case msg := <-ch3:
fmt.Println("수신:", msg)
case msg := <-ch4:
fmt.Println("수신:", msg)
}
}
}
5.3 Mutex와 동기화 프리미티브
package main
import (
"fmt"
"sync"
)
type SafeCounter struct {
mu sync.Mutex
v map[string]int
}
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
defer c.mu.Unlock()
c.v[key]++
}
func (c *SafeCounter) Value(key string) int {
c.mu.Lock()
defer c.mu.Unlock()
return c.v[key]
}
// RWMutex: 읽기는 동시, 쓰기는 독점
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Set(key, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
func (c *Cache) Get(key string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
val, ok := c.data[key]
return val, ok
}
// sync.Once: 한 번만 실행
var (
instance *Cache
once sync.Once
)
func getInstance() *Cache {
once.Do(func() {
instance = &Cache{data: make(map[string]string)}
fmt.Println("캐시 인스턴스 생성")
})
return instance
}
func main() {
counter := SafeCounter{v: make(map[string]int)}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Inc("key")
}()
}
wg.Wait()
fmt.Println("카운터:", counter.Value("key")) // 1000
// sync.Map: 동시성 안전한 맵
var sm sync.Map
sm.Store("go", "1.21")
sm.Store("python", "3.12")
sm.Range(func(key, value interface{}) bool {
fmt.Printf("%v: %v\n", key, value)
return true
})
c1 := getInstance()
c2 := getInstance()
fmt.Println(c1 == c2) // true (같은 인스턴스)
}
5.4 context.Context
package main
import (
"context"
"fmt"
"time"
)
func doWork(ctx context.Context, id int) error {
select {
case <-time.After(time.Millisecond * 200):
fmt.Printf("작업 %d 완료\n", id)
return nil
case <-ctx.Done():
fmt.Printf("작업 %d 취소: %v\n", id, ctx.Err())
return ctx.Err()
}
}
func fetchData(ctx context.Context, url string) (string, error) {
// 컨텍스트에서 값 읽기
if requestID, ok := ctx.Value("requestID").(string); ok {
fmt.Printf("요청 ID: %s, URL: %s\n", requestID, url)
}
select {
case <-time.After(time.Millisecond * 100):
return "data from " + url, nil
case <-ctx.Done():
return "", ctx.Err()
}
}
func main() {
// WithCancel
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(time.Millisecond * 50)
cancel() // 취소 신호 전송
}()
doWork(ctx, 1)
// WithTimeout
ctx2, cancel2 := context.WithTimeout(context.Background(), time.Millisecond*150)
defer cancel2()
doWork(ctx2, 2)
// WithDeadline
deadline := time.Now().Add(time.Millisecond * 300)
ctx3, cancel3 := context.WithDeadline(context.Background(), deadline)
defer cancel3()
doWork(ctx3, 3)
// WithValue: 요청 범위 데이터 전달
ctx4 := context.WithValue(context.Background(), "requestID", "req-001")
data, err := fetchData(ctx4, "https://api.example.com/users")
if err == nil {
fmt.Println("데이터:", data)
}
}
6. 패키지와 모듈
6.1 Go Modules
# 새 모듈 초기화
go mod init github.com/username/myapp
# 의존성 추가
go get github.com/gin-gonic/gin@v1.9.1
# 의존성 정리
go mod tidy
# 벤더 디렉토리 생성
go mod vendor
go.mod 파일 예시:
module github.com/username/myapp
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
github.com/stretchr/testify v1.8.4
)
6.2 패키지 구조 베스트 프랙티스
myapp/
├── cmd/
│ └── server/
│ └── main.go # 진입점
├── internal/ # 외부 패키지에서 임포트 불가
│ ├── handler/
│ │ └── user.go
│ ├── service/
│ │ └── user.go
│ └── repository/
│ └── user.go
├── pkg/ # 외부에서 임포트 가능한 라이브러리
│ └── validator/
│ └── validator.go
├── api/ # API 정의 (OpenAPI, Protobuf)
├── config/
│ └── config.go
├── go.mod
└── go.sum
6.3 주요 표준 라이브러리
package main
import (
"fmt"
"os"
"strings"
"strconv"
"time"
"io"
"bytes"
)
func main() {
// strings 패키지
s := " Hello, Go World! "
fmt.Println(strings.TrimSpace(s))
fmt.Println(strings.ToUpper(s))
fmt.Println(strings.Contains(s, "Go"))
fmt.Println(strings.Replace(s, "Go", "Golang", 1))
parts := strings.Split("a,b,c,d", ",")
fmt.Println(parts)
fmt.Println(strings.Join(parts, " | "))
// strconv 패키지
n, _ := strconv.Atoi("42")
fmt.Println(n + 1)
str := strconv.Itoa(100)
fmt.Println(str + " bottles")
f, _ := strconv.ParseFloat("3.14", 64)
fmt.Printf("%.2f\n", f)
// time 패키지
now := time.Now()
fmt.Println(now.Format("2006-01-02 15:04:05"))
fmt.Println(now.Format(time.RFC3339))
tomorrow := now.Add(24 * time.Hour)
fmt.Println(tomorrow.Sub(now))
t, _ := time.Parse("2006-01-02", "2026-03-17")
fmt.Println(t.Year(), t.Month(), t.Day())
// os 패키지
fmt.Println(os.Getenv("HOME"))
args := os.Args // 커맨드라인 인수
// io와 bytes
buf := bytes.NewBufferString("Hello")
buf.WriteString(", World")
data, _ := io.ReadAll(buf)
fmt.Println(string(data))
_ = args
}
7. 제네릭 (Go 1.18+)
7.1 제네릭 함수와 타입
package main
import (
"fmt"
"golang.org/x/exp/constraints"
)
// 타입 파라미터를 사용한 제네릭 함수
func Min[T constraints.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
func Map[T, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}
func Filter[T any](slice []T, pred func(T) bool) []T {
var result []T
for _, v := range slice {
if pred(v) {
result = append(result, v)
}
}
return result
}
func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
acc := initial
for _, v := range slice {
acc = fn(acc, v)
}
return acc
}
// 제네릭 타입
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
top := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return top, true
}
func (s *Stack[T]) Len() int {
return len(s.items)
}
// comparable 제약: == 연산자 사용 가능
func Contains[T comparable](slice []T, item T) bool {
for _, v := range slice {
if v == item {
return true
}
}
return false
}
// 커스텀 타입 제약
type Number interface {
~int | ~int32 | ~int64 | ~float32 | ~float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(Min(3, 5)) // 3
fmt.Println(Min(3.14, 2.71)) // 2.71
fmt.Println(Min("apple", "banana")) // apple
nums := []int{1, 2, 3, 4, 5}
doubled := Map(nums, func(n int) int { return n * 2 })
fmt.Println(doubled) // [2 4 6 8 10]
strs := Map(nums, func(n int) string { return fmt.Sprintf("item_%d", n) })
fmt.Println(strs)
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
fmt.Println(evens) // [2 4]
total := Reduce(nums, 0, func(acc, n int) int { return acc + n })
fmt.Println(total) // 15
var s Stack[string]
s.Push("first")
s.Push("second")
s.Push("third")
for s.Len() > 0 {
item, _ := s.Pop()
fmt.Println(item)
}
fmt.Println(Contains([]string{"go", "python", "rust"}, "go")) // true
fmt.Println(Sum([]float64{1.1, 2.2, 3.3})) // 6.6
}
8. 코드 예시: REST API 서버 (net/http)
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"sync"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
type UserStore struct {
mu sync.RWMutex
users map[int]User
nextID int
}
func NewUserStore() *UserStore {
return &UserStore{
users: make(map[int]User),
nextID: 1,
}
}
func (s *UserStore) Create(name, email string) User {
s.mu.Lock()
defer s.mu.Unlock()
user := User{ID: s.nextID, Name: name, Email: email}
s.users[s.nextID] = user
s.nextID++
return user
}
func (s *UserStore) GetAll() []User {
s.mu.RLock()
defer s.mu.RUnlock()
users := make([]User, 0, len(s.users))
for _, u := range s.users {
users = append(users, u)
}
return users
}
func (s *UserStore) GetByID(id int) (User, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
u, ok := s.users[id]
return u, ok
}
// JSON 응답 헬퍼
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
// 핸들러 팩토리 (클로저로 의존성 주입)
func makeUsersHandler(store *UserStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
users := store.GetAll()
writeJSON(w, http.StatusOK, users)
case http.MethodPost:
var body struct {
Name string `json:"name"`
Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{
"error": "invalid request body",
})
return
}
user := store.Create(body.Name, body.Email)
writeJSON(w, http.StatusCreated, user)
default:
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
}
}
}
func makeUserHandler(store *UserStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// URL에서 ID 추출: /api/users/123
parts := strings.Split(r.URL.Path, "/")
if len(parts) < 4 {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
id, err := strconv.Atoi(parts[3])
if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest)
return
}
user, ok := store.GetByID(id)
if !ok {
writeJSON(w, http.StatusNotFound, map[string]string{
"error": fmt.Sprintf("user %d not found", id),
})
return
}
writeJSON(w, http.StatusOK, user)
}
}
// 미들웨어: 로깅
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
func main() {
store := NewUserStore()
// 초기 데이터
store.Create("Alice", "alice@example.com")
store.Create("Bob", "bob@example.com")
mux := http.NewServeMux()
mux.Handle("/api/users", makeUsersHandler(store))
mux.Handle("/api/users/", makeUserHandler(store))
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
})
handler := loggingMiddleware(mux)
addr := ":8080"
fmt.Printf("서버 시작: http://localhost%s\n", addr)
log.Fatal(http.ListenAndServe(addr, handler))
}
9. 퀴즈
퀴즈 1: 슬라이스 동작 이해
다음 코드의 출력 결과는?
a := []int{1, 2, 3, 4, 5}
b := a[1:3]
b[0] = 99
fmt.Println(a)
정답: [1 99 3 4 5]
설명: 슬라이싱으로 만든 b는 a와 메모리를 공유합니다. b[0]은 a[1]과 같은 메모리 위치를 가리키므로 b[0] = 99를 하면 a[1]도 99로 변경됩니다.
퀴즈 2: 고루틴과 클로저 함정
다음 코드는 왜 문제가 있고 어떻게 수정해야 하나요?
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i)
}()
}
time.Sleep(time.Second)
정답: 대부분 5를 5번 출력하거나 예측할 수 없는 값을 출력합니다.
설명: 클로저가 변수 i를 캡처할 때 값이 아닌 참조를 캡처합니다. 고루틴이 실행될 때 이미 반복문이 끝나 i가 5가 됩니다. 수정 방법은 두 가지입니다.
1번 방법 - 인수로 전달:
for i := 0; i < 5; i++ {
go func(n int) {
fmt.Println(n)
}(i)
}
2번 방법 - 지역 변수 생성:
for i := 0; i < 5; i++ {
i := i // 새 변수 i 생성
go func() {
fmt.Println(i)
}()
}
퀴즈 3: 인터페이스와 nil
다음 코드에서 isNil의 반환값은?
type MyError struct{ msg string }
func (e *MyError) Error() string { return e.msg }
func getError() error {
var err *MyError = nil
return err
}
func main() {
err := getError()
fmt.Println(err == nil)
}
정답: false
설명: Go의 인터페이스는 타입과 값 두 가지 구성요소를 가집니다. getError()는 *MyError 타입에 nil 값인 인터페이스를 반환합니다. 이 인터페이스는 타입 정보(*MyError)를 가지고 있으므로 error 인터페이스와 비교할 때 nil이 아닙니다. nil error를 올바르게 반환하려면 return nil로 직접 반환해야 합니다.
퀴즈 4: defer 실행 순서
다음 코드의 출력 순서는?
func main() {
fmt.Println("A")
defer fmt.Println("defer 1")
fmt.Println("B")
defer fmt.Println("defer 2")
fmt.Println("C")
defer fmt.Println("defer 3")
fmt.Println("D")
}
정답: A B C D defer 3 defer 2 defer 1
설명: defer는 LIFO(Last In, First Out) 스택 방식으로 동작합니다. 일반 문장은 선언 순서대로 실행되고, defer된 함수들은 함수 종료 시 역순(나중에 defer된 것이 먼저)으로 실행됩니다.
퀴즈 5: 채널 방향성
다음 함수 시그니처에서 ch chan<- int와 ch <-chan int의 차이는?
정답:
chan<- int: 송신 전용 채널 (쓰기만 가능)<-chan int: 수신 전용 채널 (읽기만 가능)
설명: Go는 채널의 방향을 타입으로 제한할 수 있습니다. 이를 통해 함수가 채널을 어떻게 사용하는지 컴파일 타임에 검증할 수 있습니다. 예시:
func send(ch chan<- int, val int) {
ch <- val // OK
// <-ch // 컴파일 오류
}
func receive(ch <-chan int) int {
return <-ch // OK
// ch <- 1 // 컴파일 오류
}
func pipe(in <-chan int, out chan<- int) {
for v := range in {
out <- v * 2
}
}
채널 방향을 명시하면 실수로 잘못된 방향으로 채널을 사용하는 것을 방지합니다.