본 도구에서 사용하는 오픈소스 라이브러리

본 도구의 코드에는 1개의 오픈소스 라이브러리가 포함되어 있습니다.

Go 치트시트 — 빠른 레퍼런스

Go 1.22 문법, 타입, goroutine, 그리고 가장 자주 쓰이는 표준 라이브러리를 모은 빠른 레퍼런스입니다. 일상 작업의 약 80%를 다룹니다.

Go

Go Go 1.22

Go (gc toolchain) · 컴파일형 · 동시성 · 명령형 · 정적 · 강한 타입 · 구조적 타입(인터페이스)

추천 학습 경로

먼저 go run / go build와 go.mod 사용법을 익히고, 변수·타입·제어 흐름을 학습하세요. 그다음 함수·포인터·구조체를 깊이 다루고 슬라이스와 맵으로 데이터를 정리합니다. 인터페이스와 오류 처리를 이해한 뒤 goroutine과 채널로 동시성을 작성하세요. 그 후 필요할 때 http, context, 시간, 빌드/테스트를 익히면 됩니다. 함정을 다시 살펴보고 싶을 때는 FAQ 섹션을 참고하세요.

1.Hello World 및 빌드 환경

Go 프로그램 실행, go.mod 모듈, 도구 체인을 다룹니다.

최소 프로그램

모든 프로그램은 package main과 func main을 진입점으로 합니다. fmt로 출력합니다.

1
2
3
4
5
6
7
8
9
10
11
12
package main
import "fmt"
func main() {
fmt.Println("Hello, world!")
}
// 실행:
// go run hello.go
// package main 은 실행 가능 프로그램을 생성
// 다른 package 는 라이브러리
// main 함수는 반환값이 없음

실행과 빌드

go run은 컴파일 후 실행, go build는 실행 파일 생성, go install은 $GOBIN에 설치합니다.

1
2
3
4
5
6
7
8
9
10
11
12
// 직접 실행:
// go run main.go
// 패키지 전체 실행:
// go run .
// 실행 파일 빌드:
// go build -o hello .
// $GOBIN 에 설치:
// go install .
// 컴파일 검사:
// go vet ./...
// 출력 바이너리 이름은 기본적으로 모듈 이름
// Windows 에서는 .exe 생성

go.mod 모듈

go.mod는 모듈 경로와 의존성을 선언합니다. go mod init으로 초기화, go mod tidy로 정리합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 모듈 초기화:
// go mod init example.com/hello
// go.mod 내용:
// module example.com/hello
// go 1.22
// 의존성 추가:
// go get github.com/gin-gonic/gin
// 의존성 정리:
// go mod tidy
// 의존성 업데이트:
// go get -u ./...
// 의존성 캐시:
// 모든 모듈은 GOPATH/pkg/mod 에 있음
// 검증:go mod verify

패키지와 임포트

import로 표준 라이브러리와 서드파티를 가져옵니다. 같은 디렉터리의 파일은 같은 패키지입니다. 사용하지 않은 임포트는 컴파일 오류입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
import (
"fmt"
"os"
"strings"
)
// 서드파티 임포트:
// import "github.com/gin-gonic/gin"
// 별칭:
// import f "fmt"
// 빈 임포트(init 을 발동):
// import _ "net/http/pprof"
// 미사용 임포트는 컴파일 실패:
// _ 로 억제
// 패키지 이름은 임포트 경로의 마지막 세그먼트와 일치

포맷 출력

fmt.Println/Printf/Print가 기본입니다. Printf는 %s %d %v 같은 동사를 쓰고, Sprintf는 문자열을 반환합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import "fmt"
func main() {
fmt.Println("Hello") // 자동 줄바꿈
fmt.Printf("Age: %d\n", 30)
fmt.Printf("%s is %d\n", "Nick", 30)
// 자주 쓰는 동사:
// %v 기본값 %#v 구조체 %T 타입
// %d 정수 %f 부동소수점 %.2f 두 자리
// %s 문자열 %q 따옴표 포함
// %t 불리언 %x 16진수
// 문자열을 반환:
s := fmt.Sprintf("sum: %d", 1+1)
}

인자와 입력

os.Args로 명령줄 인자에 접근하고, flag 패키지로 파싱하며, fmt.Scan으로 입력을 받습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import (
"fmt"
"os"
)
func main() {
// os.Args[0] 은 프로그램 경로
args := os.Args[1:]
fmt.Println("参数:", args)
// 입력 읽기:
var name string
fmt.Scanln(&name) // 한 줄 읽기
// 프롬프트 포함:
fmt.Print("输入: ")
var n int
fmt.Scan(&n)
}

다중 파일 프로그램

같은 디렉터리의 여러 .go 파일은 같은 패키지에 속하며 패키지 스코프를 공유합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// utils.go
package main
func helper() string {
return "hi"
}
// main.go
package main
import "fmt"
func main() {
fmt.Println(helper()) // 직접 호출
}
// 실행:
// go run .
// 같은 패키지의 식별자는 서로 사용 가능
// 함수 이름이 소문자면 패키지 내부 전용
// 대문자로 패키지 외부(내보내기)

도구 체인

go version으로 버전, go env로 환경 변수, go doc으로 문서, go fmt로 포맷팅을 확인합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 버전:
// go version
// 환경 변수:
// go env GOPATH GOROOT
// go env GOPROXY # 프록시
// 문서:
// go doc fmt.Println
// go doc strings.Builder
// 포맷팅:
// gofmt -w .
// 의존성 그래프:
// go list -m all
// 툴체인 업데이트:
// go get golang.org/dl/go1.22.0
// 도움말:go help <명령>

2.변수와 상수

변수 선언, 짧은 선언, 상수, 영값, 타입 변환을 다룹니다.

변수 선언

var로 변수를 선언하며 타입은 변수명 뒤에 둡니다. 초기화하거나 영값을 받습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var name string
var age int
var ok bool
// 초기화 포함:
var count int = 5
var title = "Go" // 타입 추론
// 타입은 이름 뒤에:
var score float64
// 일괄 선언:
var (
a int
b string
)
// 초기화되지 않은 변수는 제로 값
// 선언하고 사용하지 않으면 컴파일 실패

짧은 변수 선언

:=는 타입 추론과 함께 선언과 초기화를 동시에 합니다. 함수 안에서만 사용할 수 있으며 왼쪽에 최소 하나의 새 변수가 있어야 합니다.

1
2
3
4
5
6
7
8
9
10
11
name := "Go" // string 으로 추론
count := 42
flag := true
// 기존 변수 + 새 변수:
name, age := "Nick", 30
age = 31 // 기존에는 = 사용
// 다시 선언:
// := 는 왼쪽에 최소 하나의 새 변수가 필요
// 함수 밖에서는 := 사용 불가
// 스코프 안에서 중복 선언은 오류
// x, y := 1, 2

상수

const는 컴파일 시점에 정해지는 상수입니다. iota로 자동 증가하는 열거형을 정의할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const Pi = 3.14
const (
StatusOK = 200
StatusNotFound = 404
)
// iota 로 자동 증가:
const (
A = iota // 0
B // 1
C // 2
)
// 타입 있는 상수:
const MaxSize int = 100
// 타입 없는 상수는 어떤 타입에도 사용 가능:
const N = 10
var f float64 = N // 유효
// 상수는 변경 불가

영값

초기화하지 않은 변수는 영값을 가집니다: 숫자는 0, 문자열은 "", bool은 false, 포인터는 nil. 영값도 바로 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var n int // 0
var s string // ""
var b bool // false
var p *int // nil
var arr [3]int // [0 0 0]
var m map[string]int // nil map
// 제로 값을 그대로 사용 가능:
var buf bytes.Buffer
t := buf.String() // ""
// nil 슬라이스는 append 할 수 있음:
var nums []int
nums = append(nums, 1)
// nil 맵에는 쓸 수 없음:
// m["a"] = 1 // panic
// 구조체 제로 값:var r bytes.Reader

다중 대입

한 줄에 여러 변수에 값을 대입하고 교환할 수 있습니다. 함수의 다중 반환값도 분해해서 받을 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
a, b := 1, 2
a, b = b, a // 교환
// 특정 값 무시:
x, _ := getPair()
// 여러 반환값 언패킹:
value, err := risky()
// 부분 업데이트:
map1["a"], n := "x", 1
// 동시 초기화:
// 단, := 는 최소 하나의 새 변수가 필요
// 함수 반환값의 언패킹 개수는 반드시 일치

스코프

블록 스코프가 기본입니다. 함수 안의 변수는 블록 내부에서만 보입니다. 섀도잉에 주의하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
var global = 10 // 패키지 레벨
func f() {
x := 1
if y := x + 1; y > 0 {
z := y * 2 // if 블록 안
_ = z
}
// y、z 는 여기서 보이지 않음
}
// 가리기:
func g() {
global := 99 // 패키지 레벨의 global 을 가림
_ = global
}
// 짧은 선언은 바깥 변수를 가린다
// 루프 변수는 블록 스코프

타입 변환

T(x)로 명시적 변환을 수행합니다. Go는 숫자 타입을 암묵적으로 변환하지 않으며 변환 시 오버플로와 절단이 일어날 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var i int = 42
f := float64(i) // int -> float64
// 부동소수점에서 정수로는 버림:
n := int(3.99) // 3
// 문자열에서 숫자로:
// strconv 사용:
s := strconv.Itoa(42) // "42"
n2, _ := strconv.Atoi("42")
// 바이트에서 문자열로:
s2 := string([]byte{'h', 'i'})
// 주의:string(65) 은 "A" 이고 "65" 가 아님
// rune 변환:
r := rune(20013)
// 오버플로는 버림:
// var b byte = 300 컴파일 오류

명명 규칙

첫 글자가 대문자면 패키지 외부로 공개되고, 소문자면 패키지 내부 전용입니다. 카멜케이스를 쓰고 로컬 변수는 짧게 짓습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package shapes
// 내보내기(첫 글자 대문자):
export const Area = 100
func Calculate() float64 // 내보내기
// 전용(소문자):
func helper() int // 패키지 내부
var internalCount = 0
// 캐멀 케이스:
// maxRetries / getUserById
// 두문자어는 대문자 유지:
// theURL / HTTPClient
// 권장:
// 짧은 이름은 지역 변수에 사용(i、n)
// 패키지 이름은 모두 소문자 단수형

3.데이터 타입

내장 타입, 구조체, 배열, 인터페이스, 제네릭을 다룹니다.

기본 타입

bool, string, 정수, 부동소수, 복소수, 그리고 byte/rune 별칭이 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var b bool
var s string
// 정수:
var i int // 플랫폼 의존
var i8 int8; var u uint
// 부동소수점:
var f32 float32; var f64 float64
// 복소수:
var c complex128
// 별칭:
var by byte = 'a' // uint8
var r rune = '中' // int32
// 부호 없음:
uint8 uint16 uint32 uint64
// 플랫폼:int/uint 는 64 비트
// 크기:
// byte 1 바이트 rune 4 바이트

수치 연산

산술 연산, 비트 연산, math 패키지를 사용합니다. 정수 나눗셈은 절단되며 오버플로에 주의하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
a, b := 7, 2
a / b // 3(정수 나눗셈은 버림)
a % b // 1
// 부동소수점:
// math.Sqrt(16) math.Pow(2, 3)
// math.Max/Min math.Abs
// 비트 연산:
// & | ^ << >>
// 오버플로:
// var x int8 = 127; x++ // -128
// 오버플로 감지(1.18+):
// a, ok := bits.Add(a, b, 0)
// 난수:math/rand
// 상수 연산은 임의 정밀도

struct

구조체는 필드를 묶은 타입입니다. 필드명 또는 위치로 초기화하며 태그로 메타데이터를 붙입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type User struct {
Name string
Age int
Tags []string
}
// 초기화:
u := User{Name: "Nick", Age: 30}
u2 := User{"Anna", 25, nil} // 위치 지정(비권장)
// 접근:
u.Name = "New"
// 제로 값:
var u3 User // 전부 제로 값
// 필드 태그:
// type S struct {
// ID int `json:"id"`
// }
// 포인터는 자동 역참조:
p := &u
p.Name = "X" // (*p).Name

배열

배열은 길이가 고정된 값 타입(복사됩니다)입니다. 직접 쓰기보다 슬라이스를 더 자주 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
var arr [3]int // [0 0 0]
arr[0] = 10
// 초기화:
arr2 := [3]int{1, 2, 3}
arr3 := [...]int{1, 2, 3} // 길이 추론
// 길이:len(arr)
// 값 타입:
b := arr2
b[0] = 99 // arr2 는 변하지 않음
// 비교:
// [3]int{1,2,3} == [3]int{1,2,3}
// 배열을 인자로 넘기면 복사됨
// 참조는 슬라이스나 포인터로 전달

type 정의

type으로 새 타입 또는 별칭을 정의합니다. 정의된 타입은 기존 타입과 다르며 변환이 필요합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Celsius float64
type ID string
// 새 타입:
var c Celsius = 25.0
// 메서드 집합이 달라 변환이 필요:
var f float64 = float64(c)
// 메서드:
func (c Celsius) String() string {
return fmt.Sprintf("%.1f°C", c)
}
// 타입 별칭(1.9+):
type MyInt = int // 완전히 동일
// 이름 있는 타입을 정의하면 메서드를 추가할 수 있음
// 슬라이스/map 기반 정의:
type Stack []int

제네릭

제네릭 함수는 [T any] 같은 타입 매개변수를 사용하며, 제약 인터페이스로 허용 타입을 제한합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import "fmt"
// 제네릭 함수:
func Min[T int | float64](a, b T) T {
if a < b { return a }
return b
}
// 제약:
// any 는 임의의 타입을 나타냄
// comparable 은 비교 가능
// 제네릭 타입:
type Pair[K comparable, V any] struct {
Key K
Value V
}
// 사용:
p := Pair[string, int]{Key: "a", Value: 1}
fmt.Println(Min(1, 2), Min(1.5, 2.5))
// 제약 인터페이스:
// type Number interface { ~int | ~float64 }

interface 타입

인터페이스는 메서드 집합을 정의합니다. 빈 인터페이스 interface{}(any)는 임의의 타입을 보관합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Shape interface {
Area() float64
}
// 구현:
type Circle struct{ R float64 }
func (c Circle) Area() float64 {
return 3.14 * c.R * c.R
}
// 사용:
func PrintArea(s Shape) {
fmt.Println(s.Area())
}
// 빈 인터페이스(1.18+ 는 any):
func PrintAny(v any) {
fmt.Println(v)
}
// 타입 단언:
if c, ok := s.(Circle); ok {
fmt.Println(c.R)
}
// 인터페이스 값 = 동적 타입 + 값

comparable 제약

comparable은 ==로 비교 가능한 타입을 나타내는 내장 제약입니다. 맵 키와 제네릭 비교에 사용됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// comparable 제약:
func Contains[T comparable](items []T, target T) bool {
for _, v := range items {
if v == target {
return true
}
}
return false
}
// 비교 가능한 타입:
// 숫자、문자열、bool、포인터、
// struct(필드가 모두 비교 가능)、배열
// 비교 불가:
// slice、map、func、그것을 포함한 struct
// 사용:
// Contains([]string{"a"}, "a")
// map 의 키는 비교 가능해야 함
// 제네릭 제약은 comparable 로 == 오류를 대체

4.포인터

포인터와 주소 연산: 주소 가져오기, 역참조, new/make, 스택과 힙을 다룹니다.

주소 가져오기 &

&는 변수의 주소를 가져와 포인터를 반환합니다. 포인터는 변수의 메모리 위치를 가리킵니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
n := 42
p := &n // p 는 n 을 가리키는 포인터
fmt.Println(p) // 주소
fmt.Println(*p) // 42
// 포인터 타입:*int
// 가리킨 변수는 변경 가능:
*p = 99
fmt.Println(n) // 99
// & 는 필드/배열 요소에:
// &arr[0]
// 포인터는 리터럴의 주소를 못 취함:
// &42 오류
// 구조체 주소:&User{...}

역참조 *

*p는 포인터가 가리키는 값에 접근합니다. 구조체 포인터는 자동으로 역참조됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Point struct{ X, Y int }
pt := Point{1, 2}
p := &pt
// 자동 역참조:
p.X = 10 // (*p).X
(*p).Y = 20
// 명시적 역참조가 필요한 상황:
q := new(int)
*q = 7
// 체이닝:
// (*ptr).field.method()
// 빈 포인터 역참조는 panic:
// var p *int; *p = 1 // 크래시
// 여러 레벨 포인터 * *int

nil 포인터

nil 포인터는 영값입니다. nil을 역참조하면 패닉하므로 사용 전에 검사하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var p *int // nil
// 판정:
if p != nil {
fmt.Println(*p)
} else {
fmt.Println("p 是 nil")
}
// nil 역참조는 크래시:
// *p = 1 // panic: nil pointer
// 흔한 패턴:
func safeDeref(p *int) int {
if p == nil {
return 0
}
return *p
}
// nil 을 반환해 값 없음을 표현:
// 함수는 (T, bool) 또는 *T 를 반환

포인터 매개변수

포인터 매개변수는 호출자의 변수를 변경할 수 있습니다. 값 매개변수는 복사본입니다. 큰 구조체는 포인터로 전달해 복사를 줄입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 값 인자(복사본):
func setZeroValue(n int) {
n = 0 // 복사본만 변경
}
// 포인터 인자(호출자 변경):
func setZeroPtr(n *int) {
*n = 0
}
// 사용:
x := 100
setZeroPtr(&x)
fmt.Println(x) // 0
// 구조체 포인터:
func Scale(p *Point, f int) {
p.X *= f
p.Y *= f
}
// 큰 구조체는 포인터로 넘겨 복사를 줄임
// 변경을 유지하려면 포인터 사용

new와 make

new는 값 타입에 메모리를 잡아 포인터를 반환하고, make는 슬라이스·맵·채널을 초기화합니다. 용도가 다릅니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// new:포인터와 제로 값을 반환
p := new(int)
// *p == 0
pp := new(User) // 제로 값 구조체 포인터
// make:참조 타입 초기화
nums := make([]int, 0, 10) // slice
m := make(map[string]int) // map
ch := make(chan int) // channel
// 차이:
// new(T) 는 *T 와 제로 값을 반환
// make 는 slice/map/channel 만
// 초기화된 non-nil 값을 반환
// map 은 make 해야 쓸 수 있음
// slice 는 nil 선언해도 append 가능

포인터 관용구

포인터는 가변 상태를 공유할 때 사용합니다. 반환 타입과 메서드 리시버는 의미에 맞춰 결정하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 포인터 반환:
func NewUser(name string) *User {
return &User{Name: name}
}
// 포인터 리시버 메서드:
func (u *User) SetName(name string) {
u.Name = name // 원본 객체 변경
}
// 값 리시버:
func (u User) FullName() string {
return u.Name
}
// 포인터를 쓸 때:
// 1. 리시버를 변경할 때
// 2. 큰 구조체의 복사를 피할 때
// 3. 의미적으로 공유하고 싶을 때
// 값을 쓸 때:
// 작은 구조체、불변 의미
// 포인터 일관성:같은 타입의 메서드는 통일

스택과 힙

변수가 스택과 힙 어디에 놓일지는 컴파일러의 이스케이프 분석이 결정합니다. 주소를 가져가면 힙으로 이스케이프될 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 지역 변수는 기본적으로 스택:
func local() int {
x := 42
return x // 반환값、x 는 스택 할당 가능
}
// 주소를 취하면 힙으로 도피:
func escape() *int {
x := 42
return &x // x 는 힙으로 도피
}
// 도피 확인:
// go build -gcflags='-m' .
// 도피 원인:
// 1. 지역 변수의 주소를 반환
// 2. 주소를 전역/힙 객체에 저장
// 3. 인터페이스 박싱、클로저 포획
// 도피는 GC 부하를 늘리지만 과도한 최적화는 불필요

포인터 안전성

Go 포인터에는 산술 연산이 없습니다(C 스타일 p+1 불가). GC가 수명을 관리해 댕글링 포인터가 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 포인터 연산은 없음:
// p = p + 1 오류
// 존재하지 않는 연산
// 안전한 접근(컴파일러가 보장):
// 범위 초과/댕글링이 발생하지 않음
// 포인터와 슬라이스:
nums := []int{1, 2, 3}
p := &nums[0]
// 포인터는 배열 요소를 가리킴
// unsafe 패키지로 우회(주의):
// import "unsafe"
// 변환은 수동으로 안전을 보장해야 함
// 빈 포인터 검사:역참조 전에 nil 판정
// 메모리 안전은 GC + 컴파일 시에 보장

5.제어 흐름

if, for, switch, defer, 반복 제어를 다룹니다.

if / else

if 조건에 괄호는 없습니다. 조건 앞에 짧은 초기화 문을 둘 수 있으며 조건은 반드시 불리언이어야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
score := 85
if score >= 90 {
fmt.Println("A")
} else if score >= 80 {
fmt.Println("B")
} else {
fmt.Println("C")
}
// 초기화 문:
if err := doSomething(); err != nil {
fmt.Println(err)
}
// err 은 if/else 블록 안에서만 보임
// 조건은 반드시 불리언(0이 아닌 값 판정 불가)
// 대입은 비교가 아님:
// if ok = true 오류、if ok 필요

for 루프

Go에는 for 루프만 있습니다. C 스타일 3단 형태, while 스타일 조건, 무한 루프 세 가지 형태가 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 완전한 형태:
for i := 0; i < 10; i++ {
fmt.Println(i)
}
// while 형태:
n := 0
for n < 5 {
n++
}
// 무한 루프:
for {
break // 또는 return
}
// 여러 변수:
for i, j := 0, 10; i < j; i, j = i+1, j-1 {
}
// 초기화/조건 생략:
// for ; n < 3; n++

range 반복

range는 배열, 슬라이스, 맵, 문자열, 채널을 순회합니다. Go 1.22부터 루프 변수는 매 반복마다 독립됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
nums := []int{1, 2, 3}
for i, v := range nums {
fmt.Println(i, v)
}
// 값만:
for _, v := range nums
// 인덱스만:
for i := range nums
// map 순회:
for k, v := range m { }
// 문자열 순회(rune 단위):
for i, r := range "你好" {
fmt.Println(i, r) // rune 단위
}
// channel 순회:
for v := range ch { }
// 1.22+ 는 루프 변수가 매번 독립된 복사본
// 구버전 goroutine 클로저는 주의

switch

switch는 기본적으로 fall through하지 않습니다. 여러 값, 표현식 생략(if-else 체인), 타입 스위치를 지원합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
day := "mon"
switch day {
case "mon", "tue":
fmt.Println("工作日")
case "sat", "sun":
fmt.Println("周末")
default:
fmt.Println("其他")
}
// 식 없음(불리언 조건):
score := 75
switch {
case score >= 90:
fmt.Println("A")
default:
fmt.Println("其他")
}
// 타입 판정:
var v any = "str"
switch t := v.(type) {
case string:
fmt.Println("string", t)
case int:
fmt.Println("int", t)
}

defer

defer는 함수 종료 시 실행될 작업을 등록합니다. LIFO 순서로 실행되며 자원 정리에 자주 쓰입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func readFile() error {
f, err := os.Open("data.txt")
if err != nil {
return err
}
defer f.Close() // 함수 종료 시 닫기
// 처리...
return nil
}
// 여러 defer 는 후입선출:
func example() {
defer fmt.Println("first")
defer fmt.Println("second") // 먼저 출력됨
}
// defer 인자는 선언 시 평가:
func count() {
i := 0
defer fmt.Println(i) // 0
i = 10
}
// 용도:
// 파일/연결 닫기、잠금 해제、panic 복구

break와 continue

break는 루프를 빠져나가고 continue는 다음 반복을 건너뜁니다. 레이블로 중첩 루프도 제어할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue
}
if i > 7 {
break
}
fmt.Println(i) // 1 3 5 7
}
// 라벨로 바깥쪽 제어:
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
continue outer // 바깥쪽의 다음 반복으로
}
fmt.Println(i, j)
}
}
// break outer 로 바깥 루프 종료
// 라벨은 루프와 같은 줄이나 앞 줄에 배치

초기화가 있는 switch

switch 앞에 초기화 문을 둘 수 있습니다. 오류 처리 체인에서 유용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
switch n := rand.IntN(3); n {
case 0:
fmt.Println("零")
case 1:
fmt.Println("一")
default:
fmt.Println("其他")
}
// 초기화 변수는 switch 안에서만 보임
// 동등한 작성법:
// n := rand.IntN(3)
// switch n { ... }
// 용도:오류 처리 체인:
// switch err := f(); err {
// case nil: ...
// default: log(err)
// }
// case 가 일치하지 않으면 자동으로 default 로

goto와 레이블

goto는 레이블로 점프합니다. Go에서 지원은 하지만 거의 쓰이지 않으며, 깊게 중첩된 오류 처리에 제한적으로 사용됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func process(items []int) error {
for i, v := range items {
if v < 0 {
goto failure
}
_ = i
}
return nil
failure:
return errors.New("发现负数")
}
// 주의점:
// 라벨은 사용 이후(뒤)에 선언해야 함
// 변수 선언 안으로/를 뛰어넘을 수 없음
// goto 는 다른 블록으로 점프 불가
// 많은 상황에서 defer/오류 반환이 더 명확
// 신중하게 사용、가독성 우선

6.함수

함수 정의, 다중 반환값, 클로저, 메서드, 함수 값을 다룹니다.

함수 정의

func로 함수를 정의합니다. 매개변수는 타입을 가지고 반환 타입은 마지막에 둡니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import "fmt"
func add(a int, b int) int {
return a + b
}
// 인자 타입 생략:
func sub(a, b int) int {
return a - b
}
// 반환값 없음:
func log(msg string) {
fmt.Println(msg)
}
// 호출:
func main() {
sum := add(1, 2)
fmt.Println(sum)
}
// 함수 인자는 값 전달(복사본)
// 참조는 포인터/slice/map 으로 전달

다중 반환값

Go 함수는 여러 값을 반환할 수 있습니다. 관용적으로 (값, error) 형태를 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func div(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("除数为零")
}
return a / b, nil
}
// 사용:
result, err := div(10, 0)
if err != nil {
fmt.Println("错误:", err)
} else {
fmt.Println(result)
}
// 일부 무시:
q, _ := div(10, 2)
// 여러 유효 값:
func minMax(nums []int) (int, int) {
return nums[0], nums[len(nums)-1]
}
// 관례:
// 마지막 반환값은 error
// 오류 시 다른 반환값은 제로 값으로

이름 있는 반환값

반환값에 이름을 붙일 수 있습니다. 함수 안에서 대입하고 bare return으로 보낼 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return // 맨 return 으로 x, y
}
// 사용 상황:
func stats(nums []int) (sum, avg float64) {
sum = 0
for _, v := range nums {
sum += float64(v)
}
avg = sum / float64(len(nums))
return
}
// 주의:
// 맨 return 은 가독성 논란이 있음
// defer 는 이름 있는 반환값을 변경 가능
// 이름 있는 반환값은 제로 값을 가짐
// 복잡한 반환값은 이름 있는 방식이 명확

가변 인자

...T로 임의 개수의 인자를 슬라이스로 모읍니다. 호출 시 slice...로 펼쳐 전달합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
// 호출:
sum(1, 2, 3) // 6
sum() // 0
// 슬라이스 펼치기:
nums := []int{1, 2}
sum(nums...) // 여러 인자로 펼침
// 혼합:
// func greet(prefix string, names ...string)
// 가변 인자는 마지막이어야 함
// 타입은 []T(슬라이스와 동일)

클로저

클로저는 바깥 변수를 캡처합니다. 반환된 함수는 자신의 상태를 유지합니다. 1.22부터 루프 변수는 반복마다 캡처됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func makeCounter() func() int {
count := 0
return func() int {
count++ // count 포획
return count
}
}
// 사용:
counter := makeCounter()
counter() // 1
counter() // 2
// 클로저가 포획하는 것은 변수 자체:
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
// 각자 독립된 상태:
c1, c2 := makeCounter(), makeCounter()
// 1.22+ 는 루프 변수가 매번 독립

메서드

메서드는 리시버를 통해 타입에 묶이며, 리시버는 func와 메서드 이름 사이에 적습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Rectangle struct {
Width, Height float64
}
// 값 리시버:
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// 포인터 리시버:
func (r *Rectangle) Scale(f float64) {
r.Width *= f
r.Height *= f
}
// 사용:
rect := Rectangle{2, 3}
fmt.Println(rect.Area())
rect.Scale(2)
// 메서드와 함수의 차이:
// 메서드는 타입에 묶임
// 메서드는 자신의 패키지 타입에만 정의 가능
// 포인터 리시버는 원본 객체를 변경 가능

함수 값

함수는 1급 시민으로 변수에 대입하거나 인자/반환값으로 쓸 수 있습니다. 함수 타입은 시그니처로 표현됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 함수 타입:
type Operation func(a, b int) int
func add(a, b int) int { return a + b }
func apply(op Operation, x, y int) int {
return op(x, y)
}
// 사용:
result := apply(add, 3, 4) // 7
// 익명 함수:
f := func(a, b int) int { return a - b }
// 인자로 전달(콜백):
func filter(nums []int, pred func(int) bool) []int {
out := make([]int, 0)
for _, n := range nums {
if pred(n) {
out = append(out, n)
}
}
return out
}
// 자주 사용:sort.Slice 에 비교 함수 전달

init 함수

init은 패키지가 임포트될 때 자동으로 실행됩니다. 한 파일에 여러 init을 둘 수 있으며 초기화에 사용됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import "fmt"
var config map[string]string
func init() {
config = make(map[string]string)
config["env"] = "dev"
fmt.Println("初始化完成")
}
// 특징:
// main 앞에 실행됨
// 각 파일에 여러 init 을 작성 가능
// 여러 init 은 파일/순서에 따라 실행
// 의존 패키지의 init 이 먼저 실행
// 용도:
// 드라이버 등록、설정 초기화
// var 로 초기화할 수 없을 때
// init 순서에 의존하는 것은 피할 것

7.문자열

불변 문자열, rune/바이트, strings 패키지, 포맷팅을 다룹니다.

문자열 기초

문자열은 불변의 바이트열입니다. 큰따옴표는 일반 문자열, 백틱은 로 문자열 리터럴입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
s := "hello"
// 백틱 원시 문자열:
raw := `多行
文本`
// 길이(바이트 수):
len("hello") // 5
len("你好") // 6(UTF-8 은 한 글자 3 바이트)
// 인덱스(바이트):
s[0] // 'h' (byte)
// 연결:
s2 := "a" + "b"
// 불변:
// s[0] = 'x' 오류
// 복사:
// 문자열 대입은 참조 복사(불변이라 안전)
// 문자열은 비교 가능:== < >

rune과 문자

rune은 유니코드 코드 포인트(int32)입니다. range는 rune 단위로 순회하며 unicode/utf8 패키지가 디코딩을 도와줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
s := "你好, Go"
// range 는 rune 단위:
for _, r := range s {
fmt.Printf("%c ", r)
}
// 你好 , 공백 G o
// rune 슬라이스로 변환:
rs := []rune(s)
fmt.Println(len(rs)) // rune 수
// 바이트 슬라이스:
bs := []byte(s)
// rune 리터럴:
ch := '中' // rune
// 디코딩:
import "unicode/utf8"
// utf8.RuneCountInString(s)
// utf8.DecodeRuneInString
// 인덱스는 바이트 위치、len 으로 중국어를 자르지 말 것

strings.Builder

Builder는 문자열을 효율적으로 누적합니다. 루프 안의 + 결합보다 할당이 적습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "strings"
var b strings.Builder
b.WriteString("hello")
b.WriteString(", ")
b.WriteString("world")
result := b.String() // "hello, world"
// 사전 할당:
b.Grow(100) // 사전 용량
// 바이트/문자 쓰기:
b.WriteByte('!')
b.WriteRune('中')
// 길이 반환:
b.Len()
// 용도:
// 루프에서의 연결、HTML/CSV 생성
// + 연결보다 효율적(여러 번의 할당 회피)
// bytes.Buffer 보다 문자열 의미에 부합

strings 패키지

Contains, Split, Join, Trim, Replace 등 다양한 문자열 연산이 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import "strings"
s := " Hello World "
strings.TrimSpace(s)
strings.ToUpper(s)
strings.ToLower(s)
strings.Contains(s, "World") // true
strings.HasPrefix(s, " ")
strings.HasSuffix(s, " ")
strings.Replace(s, "World", "Go", -1)
strings.ReplaceAll(s, " ", "-")
// 분할:
parts := strings.Split("a,b,c", ",")
strings.Join([]string{"a", "b"}, "-")
// 부분 문자열:
strings.Index(s, "World")
strings.LastIndex(s, "o")
// 세기:
strings.Count(s, "o")
// 비교:
strings.Compare("a", "b")
strings.EqualFold("Go", "go") // 대소문자 무시

fmt 포맷팅

Printf/Sprintf의 동사와 폭, 정밀도, 채움을 제어할 수 있습니다. Fprintf는 io.Writer에 씁니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
name, age := "Nick", 30
fmt.Printf("%s 今年 %d 岁\n", name, age)
// 동사:
// %v 기본값 %+v 필드명 포함 %#v Go 문법
// %d 10진수 %x 16진수
// %f 부동소수점 %.2f 두 자리
// %s 문자열 %q 따옴표 포함
// %t 불리언 %T 타입
// %5d 폭 %-5d 왼쪽 정렬
// %05d 0 채움
// %10.2f 폭 10 두 자리
// 문자열을 반환:
s := fmt.Sprintf("%s-%d", name, age)
// io.Writer 에 쓰기:
fmt.Fprintf(buf, "%s", name)

strconv 변환

문자열과 숫자/불리언 상호 변환, 그리고 따옴표 처리를 다룹니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "strconv"
// 숫자를 문자열로:
s := strconv.Itoa(42) // "42"
// 문자열을 숫자로:
n, err := strconv.Atoi("42")
// 부동소수점 파싱:
f, err := strconv.ParseFloat("3.14", 64)
// 진법 지정:
n64, _ := strconv.ParseInt("101", 2, 64)
// 포맷팅:
strconv.FormatInt(255, 16) // "ff"
strconv.FormatFloat(3.14, 'f', 2, 64)
// 불리언:
b, _ := strconv.ParseBool("true")
strconv.FormatBool(true)
// 따옴표:
strconv.Quote("hi") // "\"hi\""
// 진법 포맷팅:
fmt.Sprintf("%x", 255) // "ff"

인코딩 처리

base64, hex, URL 인코딩을 다루고 []byte와 문자열 사이의 변환도 함께 알아봅니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import (
"encoding/base64"
"encoding/hex"
"net/url"
)
// base64:
b64 := base64.StdEncoding.EncodeToString([]byte("hi"))
decoded, _ := base64.StdEncoding.DecodeString(b64)
// hex:
h := hex.EncodeToString([]byte("\x00\xff"))
// URL 인코딩:
url.QueryEscape("a b&c") // "a+b%26c"
// 바이트와 문자열:
s := string([]byte{104, 105})
bs := []byte(s)
// 16진수 출력:
fmt.Printf("%x\n", "hi")
// 인코딩/디코딩 오류는 err 처리

문자 순회

비 ASCII 텍스트는 rune 단위로 순회하는 것이 안전합니다. 인덱스는 바이트 단위임을 잊지 마세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
s := "你好 Go"
// range 로 rune 단위 순회:
for _, r := range s {
fmt.Printf("%c", r)
}
// 인덱스가 필요한 경우:
for i, r := range s {
fmt.Println(i, r) // 바이트 오프셋 + 코드포인트
}
// 바이트 인덱스(순수 ASCII):
for i := 0; i < len(s); i++ {
fmt.Println(s[i])
}
// []rune 으로 변환:
rs := []rune(s)
// n 번째 문자 가져오기:
fmt.Println(string(rs[2]))
// 중국어는 반드시 rune 단위로 처리

8.컬렉션

슬라이스, 맵, 배열, 정렬을 다룹니다.

슬라이스 slice

슬라이스는 내부 배열에 대한 동적 뷰입니다. nil 슬라이스도 길이 0이며 append 등에 사용 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
nums := []int{1, 2, 3} // 리터럴
var s []int // nil slice
// 추가:
nums = append(nums, 4)
// 빈 slice:
empty := []int{}
// 길이와 용량:
len(nums); cap(nums)
// 슬라이싱:
sub := nums[1:3] // 뷰가 기본 배열 공유
// 기본 배열 참조:
// sub 를 변경하면 nums 에 영향
// append 가 용량을 넘으면 새 배열 할당
// make 로 용량 지정:
make([]int, 5, 10)

슬라이스 연산

append로 추가, copy로 복제, 그리고 append 관용구로 삭제/삽입을 수행합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
nums := []int{1, 2, 3, 4, 5}
// 여러 개 추가:
nums = append(nums, 6, 7)
// 슬라이스 연결:
nums = append(nums, []int{8, 9}...)
// 인덱스 2 삭제:
nums = append(nums[:2], nums[3:]...)
// 복사:
dst := make([]int, len(nums))
copy(dst, nums)
// 앞에 삽입:
nums = append([]int{0}, nums...)
// 비우기:
nums = nums[:0]
// 뒤집기:
for i, j := 0, len(nums)-1; i < j; i, j = i+1, j-1 {
nums[i], nums[j] = nums[j], nums[i]
}
// 복사는 기본 배열 공유에 주의

make와 용량

make는 슬라이스를 만들면서 용량을 미리 잡습니다. append 중 잦은 재할당을 줄여주며 cap으로 용량 증가 전략을 제어합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 길이 0 용량 5:
nums := make([]int, 0, 5)
// 사전 할당으로 여러 번의 복사 회피:
nums = append(nums, 1, 2, 3)
// 용량 증가:
// append 가 용량을 넘으면 두 배로 확장
// 큰 슬라이스는 사전 할당:
// make([]int, 0, 100000)
// 길이와 용량:
s := make([]int, 5) // 길이 5、제로 값
cap(s) // 5
// 비우고 용량 유지:
s = s[:0]
// 빈 판정:
if len(s) == 0
// cap 으로 사전 할당해 성능 최적화

map

맵은 키/값 쌍을 저장합니다. make로 생성하고 읽기/쓰기/삭제/순회가 가능합니다. nil 맵은 읽기 전용입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
m := make(map[string]int)
m["a"] = 1
m["b"] = 2
// 읽기:
v := m["a"] // 없으면 제로 값 반환
v, ok := m["a"] // ok 로 존재 판정
if val, ok := m["x"]; ok {
fmt.Println(val)
}
// 삭제:
delete(m, "a")
// 순회(순서 없음):
for k, v := range m {
fmt.Println(k, v)
}
// 길이:len(m)
// 리터럴:
mm := map[string]int{"a": 1}
// nil map:
// var mm map[string]int
// mm["a"] = 1 // panic

map 관용구

맵을 집합, 카운터, 캐시로 활용합니다. 키를 기준으로 값을 모을 때도 유용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 집합(set):
seen := make(map[string]bool)
for _, v := range list {
if !seen[v] {
seen[v] = true
fmt.Println("新:", v)
}
}
// 카운트:
counts := make(map[string]int)
for _, w := range words {
counts[w]++
}
// 캐시:
var cache = make(map[string][]byte)
func get(key string) ([]byte, bool) {
data, ok := cache[key]
return data, ok
}
// 구조체 값:
m := map[string]Point{}
// 중첩 업데이트:
// 값 복사 함정:p := m["a"]; p.X=1; m["a"]=p

정렬

sort 패키지로 슬라이스를 정렬합니다. 비교 함수를 직접 전달할 수 있고 sort.Search로 이분 탐색도 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import "sort"
// 기본 정렬:
nums := []int{3, 1, 2}
sort.Ints(nums)
strs := []string{"b", "a"}
sort.Strings(strs)
// 커스텀 비교:
type Person struct{ Name string; Age int }
people := []Person{{Age: 30}, {Age: 20}}
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age
})
// 역순:
sort.Slice(people, func(i, j int) bool {
return people[i].Age > people[j].Age
})
// 안정 정렬:
sort.SliceStable(people, ...)
// 이진 탐색:
// sort.SearchInts(nums, 2)
// 정렬 여부:sort.IntsAreSorted(nums)

container 패키지

container/heap은 힙, container/list는 양방향 연결 리스트를 제공합니다. 자료구조가 필요할 때 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// container/list(연결 리스트):
import "container/list"
l := list.New()
l.PushBack(1)
l.PushFront(0)
for e := l.Front(); e != nil; e = e.Next() {
fmt.Println(e.Value)
}
// 힙:
import "container/heap"
// heap.Interface 구현:
type IntHeap []int
func (h IntHeap) Len() int { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *IntHeap) Push(x any) { *h = append(*h, x.(int)) }
func (h *IntHeap) Pop() any {
old := *h; n := len(old); x := old[n-1]
*h = old[:n-1]; return x
}
// 사용:
h := &IntHeap{3, 1, 2}
heap.Init(h)
heap.Pop(h) // 최소 1

반복 팁

range로 인덱스나 값을 받고, 맵은 키를 정렬해 순회하면 안정적인 순서를 얻습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 인덱스 순회:
for i := range nums {
nums[i] *= 2
}
// 인덱스 + 값:
for i, v := range nums {
fmt.Println(i, v)
}
// 값만:
for _, v := range nums
// 문자열 rune:
for i, r := range s
// map 키 정렬 순회:
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, m[k])
}
// 슬라이스 역순 순회:
for i := len(nums) - 1; i >= 0; i--

9.메모리와 성능

가비지 컬렉션, 이스케이프 분석, 할당 최적화, 메모리 프로파일링을 다룹니다.

가비지 컬렉션

Go는 동시·저지연 자동 GC를 제공합니다. free를 직접 호출할 필요가 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Go 는 동시 마크스윕 GC 사용
// 수동 malloc/free 불필요
// 할당:
// 힙 객체는 GC 가 회수
// 스택 객체는 함수가 돌아오면 즉시 해제
// GC 매개변수:
// GOGC=100 기본 임계값
// debug.SetGCPercent()
// 트리거:
// 메모리 2배/명시적 runtime.GC()
// 영향:
// 힙 할당을 줄이면 GC 부하가 낮아짐
// 많은 소형 객체는 sync.Pool 사용
// 큰 객체는 수명 주기 주의

이스케이프 분석

변수의 배치 위치는 컴파일러가 결정합니다. 주소를 가져가거나 인터페이스로 박싱하면 힙으로 이스케이프할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 스택 할당:
func local() int {
x := 42 // 도피하지 않음、스택 위
return x
}
// 힙 할당(도피):
func leak() *int {
x := 42
return &x // 주소가 반환되어 도피
}
// 도피 확인:
// go build -gcflags='-m' ./...
// 도피 트리거:
// 1. 지역 변수의 주소를 반환
// 2. 클로저가 변수 포획
// 3. 인터페이스에 값 저장(박싱)
// 4. 전역 변수에 대입
// 최적화:
// 불필요한 도피를 피해 GC 부하 감소
// 단, 과도한 최적화는 불필요

할당 최적화

용량을 미리 잡고 버퍼를 재사용하며 작은 할당을 줄여 핫패스를 최적화합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 슬라이스 사전 할당:
nums := make([]int, 0, n) // 반복 확장 회피
// 버퍼 재사용:
var buf bytes.Buffer
buf.Grow(1024) // 사전 할당
// 임시 객체 회피:
// 루프 안에서 fmt.Sprintf 생성 회피
// builder 로 연결:
var sb strings.Builder
// 객체 풀:
// 짧은 수명 객체 재사용
// 인터페이스 호출 오버헤드:
// 핫 경로에서 인터페이스 경유 회피
// 분석이 최적화에 앞선다:
// pprof 로 핫스팟 찾기
// 일반 코드를 너무 일찍 최적화하지 말 것

파이너라이저

runtime.SetFinalizer는 객체 회수 직전 정리 함수를 등록합니다. 외부 자원의 안전망으로 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import "runtime"
type Resource struct {
name string
handle *os.File
}
func NewResource(name string) *Resource {
r := &Resource{name: name}
runtime.SetFinalizer(r, func(r *Resource) {
if r.handle != nil {
r.handle.Close() // 보험 정리
}
})
return r
}
// 주의점:
// 파이널라이저는 즉시 실행을 보장하지 않음
// 프로그램 종료 시 미처리 파이널라이저는 실행되지 않음
// 모범 사례:
// 명시적 Close 를 우선
// 파이널라이저는 보험으로만
// 순환 참조는 파이널라이저가 영원히 실행되지 않을 수 있음

메모리 모델

동시성 환경에서 쓰기는 채널, 락, atomic 같은 동기화 이벤트가 있을 때만 다른 goroutine에게 보입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Go 메모리 모델:
// 한 goroutine 의 쓰기가
// 다른 goroutine 의 읽기 전에 보이려면,
// 동기화 프리미티브가 필요:
// 1. channel:
ch := make(chan int)
gr() { ch <- 1 }() // 쓰기
<-ch // 읽기
// 2. 락:
var mu sync.Mutex
mu.Lock()
// 임계 영역
mu.Unlock()
// 3. atomic:
var x atomic.Int64
x.Store(1)
v := x.Load()
// 동기화 없는 공유 읽기/쓰기는 데이터 레이스
// go run -race 로 감지

메모리 프로파일링

pprof로 메모리와 CPU를 프로파일링할 수 있습니다. 힙 스냅샷은 누수 추적에 유용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import (
_ "net/http/pprof"
"net/http"
)
// pprof 시작:
// go run -race main.go
// 코드 안에서:
func main() {
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
// ...
}
// 분석:
// go tool pprof http://localhost:6060/debug/pprof/heap
// go tool pprof http://localhost:6060/debug/pprof/profile
// 명령줄 상호작용:
// top / list / web
// 플레임 그래프:
// go tool pprof -http=:8080 profile.out
// 특정:할당 핫스팟、누수 객체

sync.Pool

Pool은 수명이 짧은 객체를 재활용해 GC 부담을 줄입니다. 버퍼·디코더에 잘 어울립니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "sync"
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
// 사용:
b := bufPool.Get().(*bytes.Buffer)
b.Reset() // 재사용 전 정리
// 쓰기에 사용...
bufPool.Put(b) // 반납
// 적용:
// 많은 짧은 수명 객체
// 임시 버퍼、직렬화 버퍼
// 주의:
// GC 는 Pool 을 비움(보존 미보장)
// 부적합:장기 보유 객체
// Get 은 nil 을 반환할 수 있음(New 미설정)
// 상태 있는 객체를 Pool 에 넣지 말 것

스택과 재귀

goroutine의 스택은 동적으로 자라지만 깊게 중첩된 재귀에는 한계가 있습니다. 너무 깊은 재귀는 루프로 바꾸세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// goroutine 스택:
// 초기 2KB、동적 확장
// 스택 상한 약 1GB(64비트)
// 재귀:
func factorial(n int) int {
if n <= 1 {
return 1
}
return n * factorial(n-1)
}
// 깊은 재귀의 위험:
// 스택 오버플로 크래시:fatal error: stack overflow
// 깊은 재귀는 반복으로:
func factorialIter(n int) int {
result := 1
for i := 2; i <= n; i++ {
result *= i
}
return result
}
// 재귀 깊이 메모:
// 수만 층은 스택 증가 주의

10.구조체와 메서드

Go의 OOP: 구조체, 인터페이스, 임베딩과 합성(클래스 상속 없음).

struct와 메서드

구조체는 데이터를 담고 메서드로 동작을 부여합니다. 생성자 함수로 인스턴스를 만드는 것이 관례입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Account struct {
ID string
Balance float64
}
// 값 리시버:
func (a Account) Balance() float64 {
return a.Balance
}
// 포인터 리시버:
func (a *Account) Deposit(amount float64) {
a.Balance += amount
}
// 생성자:
func NewAccount(id string) *Account {
return &Account{ID: id}
}
// 사용:
acc := NewAccount("a1")
acc.Deposit(100)
// 값/포인터 자동 변환:
// acc 포인터는 값 메서드를 호출 가능

리시버 선택

값 리시버는 원본을 바꾸지 않고 포인터 리시버는 바꿉니다. 같은 타입의 메서드들은 리시버 종류를 통일하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Counter struct {
N int
}
// 값 리시버(복사본):
func (c Counter) Value() int {
return c.N
}
// 포인터 리시버(변경):
func (c *Counter) Inc() {
c.N++
}
// 사용:
c := Counter{}
c.Inc() // 자동 주소 취득
// 규칙:
// 리시버 변경 -> 포인터
// 큰 구조체 -> 포인터(복사 회피)
// 모든 메서드의 리시버 통일:
// 혼용하면 자동 주소 취득이 발생
// 인터페이스 구현도 리시버 일치 필요

인터페이스

인터페이스는 동작 계약을 정의하며 메서드 집합이 일치하면 타입이 암묵적으로 구현합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Greeter interface {
Greet() string
}
// 타입 구현:
type Dog struct{ Name string }
func (d Dog) Greet() string {
return "汪汪 " + d.Name
}
type Cat struct{ Name string }
func (c Cat) Greet() string {
return "喵 " + c.Name
}
// 사용:
func GreetAll(gs []Greeter) {
for _, g := range gs {
fmt.Println(g.Greet())
}
}
// 구현은 암묵적:
// 메서드 집합이 일치하면 구현한 것
// implements 선언 불필요
// 인터페이스 값은 구체 타입을 포함
// 인터페이스를 인자로 하면 결합 분리

인터페이스와 nil

nil 인터페이스와 타입 있는 nil 포인터를 담은 인터페이스는 다릅니다. nil 판별에 주의하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Error interface {
Error() string
}
// 함정:
func returnsNil() *MyError {
return nil
}
var err error = returnsNil()
// err != nil 이지만 내부는 nil 포인터
if err != nil {
fmt.Println("有错误") // 오판
}
// 원인:인터페이스 = 타입 + 값
// nil 포인터에도 타입 정보가 있음
// 해결:
// 1. *T 가 아니라 error 를 반환
// 2. 빈 판정은 리플렉션:
// reflect.ValueOf(err).IsNil()
// 3. 인터페이스 안에 nil 포인터를 넣지 않는 규약
// error 를 반환할 때 nil 은 error 의 nil 임을 보장

임베딩

임베드한 구조체나 인터페이스의 필드와 메서드는 자동으로 승격됩니다. 상속보다 합성을 우선하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type Base struct {
ID int
}
func (b Base) GetID() int { return b.ID }
type User struct {
Base // 임베딩
Name string
}
// 승격:
u := User{Base: Base{ID: 1}, Name: "Nick"}
u.GetID() // 승격된 메서드
u.ID // 승격된 필드
// 오버라이드:
type Admin struct {
Base
Permissions []string
}
func (a Admin) GetID() int {
return a.Base.ID * 100 // 커스텀
}
// interface 임베딩도 가능:
// type Reader interface { io.Reader }
// 상속 대신 조합:
// 필드 + 메서드 승격

합성

Go는 상속 대신 합성으로 재사용을 표현합니다. 작은 인터페이스를 조합해 큰 인터페이스를 만듭니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 인터페이스 조합:
type Reader interface {
Read([]byte) (int, error)
}
type Writer interface {
Write([]byte) (int, error)
}
type ReadWriter interface {
Reader
Writer
}
// 구조체 조합:
type File struct {
name string
}
type GzipFile struct {
File // 파일 처리 재사용
level int
}
// 조합은 재사용을 제공하고 상속의 취약함이 없음
// 작은 인터페이스 원칙:
// 인터페이스가 작을수록 만족하기 쉬움
// 필요할 때만 조합
// 구조의 중첩으로 기능 재사용

타입 어서션

인터페이스를 구체 타입으로 단언합니다. ok 패턴으로 안전하게 검사하고, type switch로 여러 분기를 처리하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var v any = "hello"
// 안전한 단언:
s, ok := v.(string)
if ok {
fmt.Println(s)
}
// 안전하지 않은 단언(실패 시 panic):
// s := v.(string)
// type switch:
switch val := v.(type) {
case string:
fmt.Println("string", val)
case int:
fmt.Println("int", val)
default:
fmt.Println("未知", val)
}
// 단언 실패는 제로 값 + false 반환
// 더 구체적인 인터페이스로의 단언:
// if r, ok := w.(io.Reader); ok

any / 빈 인터페이스

any는 interface{}의 별칭으로 임의 타입을 보관합니다. 사용할 때는 타입 어서션이 필요합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// any 는 interface{} 의 별칭:
var v any
v = 42
v = "str"
// 꺼내려면 단언 필요:
s := v.(string)
// 제네릭으로 대체:
// 타입 매개변수가 필요하면 제네릭:
func Print[T any](v T) {
fmt.Println(v)
}
// 빈 인터페이스를 컨테이너로:
var store = make(map[string]any)
store["age"] = 30
store["name"] = "Nick"
// 꺼낼 때 타입 단언:
age, ok := store["age"].(int)
// 신중하게:
// 타입 안전성 상실
// 명시적 타입/제네릭 우선
// 직렬화 시 JSON 에서 any 가 일반적

11.오류 처리

error 인터페이스, 오류 래핑, errors.Is/As, panic/recover를 다룹니다.

error 인터페이스

error는 Error() string 한 메서드를 가진 내장 인터페이스입니다. 예외 대신 오류 값을 반환합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type error interface {
Error() string
}
// 오류 반환:
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("除数为零")
}
return a / b, nil
}
// 판정:
result, err := divide(10, 0)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(result)
// 커스텀 오류 타입:
type ValidationError struct {
Field string
}
func (e ValidationError) Error() string {
return "字段 " + e.Field + " 无效"
}
// nil 오류는 성공을 의미

오류 반환

오류는 호출 체인을 따라 위로 전파됩니다. 각 단계에서 err != nil을 확인하고 처리합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func openConfig(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err // 그대로 전파
}
if len(data) == 0 {
return nil, errors.New("配置为空")
}
return data, nil
}
// 호출 체인:
func load() error {
data, err := openConfig("app.json")
if err != nil {
return err // 상위로 계속 전파
}
_ = data
return nil
}
// 관례:
// 오류 시 제로 값 + err 반환
// err 가 nil 일 때 다른 반환값은 유효
// 오류를 삼키지 않는다:
// 최소한 log 하거나 반환

오류 메시지

fmt.Errorf로 포맷된 오류, errors.New로 단순 오류를 만듭니다. %w로 래핑할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import "fmt"
// 단순 오류:
// errors.New("失败")
// 매개변수 포함:
func open(name string) error {
return fmt.Errorf("无法打开 %s", name)
}
// 다층 래핑:
// %w 로 하위 오류 래핑
func load() error {
err := readFile()
if err != nil {
return fmt.Errorf("load 失败: %w", err)
}
return nil
}
// %v 는 래핑하지 않음:
// fmt.Errorf("失败: %v", err)
// 새 오류 인스턴스:
// errors.New("message")
// 커스텀:
// fmt.Errorf("状态码 %d", 404)

errors.Is

errors.Is는 래핑 체인을 따라 일치 여부를 판단합니다. err == 비교보다 이쪽을 권장합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "errors"
var ErrNotFound = errors.New("not found")
func find(id int) error {
return fmt.Errorf("查找 %d: %w", id, ErrNotFound)
}
// 판정(래핑 지원):
err := find(42)
if errors.Is(err, ErrNotFound) {
fmt.Println("未找到")
}
// 여러 타깃:
// errors.Is(err, ErrNotFound) || errors.Is(err, ErrTimeout)
// 단일 타깃:
// 센티널 오류 정의 var ErrX = errors.New(...)
// == 은 %w 래핑을 통과하지 못함
// 표준 라이브러리 사용법:
// errors.Is(err, os.ErrNotExist)

errors.As

errors.As는 래핑 체인에서 지정한 타입의 오류를 찾아 추출합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import "errors"
type ConfigError struct {
Path string
Msg string
}
func (e *ConfigError) Error() string {
return e.Path + ": " + e.Msg
}
// 래핑해서 던지기:
func loadConfig() error {
return fmt.Errorf("加载: %w",
&ConfigError{Path: "app.json", Msg: "语法错误"})
}
// 추출:
err := loadConfig()
var configErr *ConfigError
if errors.As(err, &configErr) {
fmt.Println(configErr.Path, configErr.Msg)
}
// Is 와의 차이:
// Is 는 오류 값을 비교(센티널)
// As 는 오류 타입을 추출(필드 포함)
// 커스텀 오류는 포인터 리시버
// 서드파티 라이브러리 오류에 자주 사용

panic과 recover

panic은 goroutine을 중단시키고 recover는 defer 안에서만 동작합니다. 복구 불가능한 상황에서만 사용하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// panic:
func divide(a, b int) int {
if b == 0 {
panic("除数为零") // 크래시
}
return a / b
}
// recover 로 포착:
func safeCall(f func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
f()
return nil
}
// 사용:
// err := safeCall(func() { divide(1, 0) })
// 규칙:
// panic 은 defer 안에서만 recover
// 프로그램이 계속할 수 없는 오류에만 사용
// panic 을 일반 오류 처리에 사용하지 말 것
// 라이브러리 코드에서는 panic 을 피할 것

defer에서 오류 처리

이름 있는 반환값을 사용하면 defer에서 반환값에 오류를 대입할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 파일 닫기 오류 검사:
func read() (err error) {
f, err := os.Open("data.txt")
if err != nil {
return err
}
defer func() {
if closeErr := f.Close(); closeErr != nil {
err = closeErr // 반환값 덮어쓰기
}
}()
// 처리...
return nil
}
// 연결 닫기:
// defer conn.Close()
// 잠금 해제:
// mu.Lock()
// defer mu.Unlock()
// 임시 파일 정리:
// defer os.Remove(tmp)
// defer 인자는 즉시 평가됨에 주의

오류 결합

errors.Join은 여러 오류를 하나로 합칩니다. 다중 검증이나 작업 모음에 유용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import "errors"
// 여러 오류 결합:
err1 := errors.New("数据库失败")
err2 := errors.New("缓存失败")
joined := errors.Join(err1, err2)
fmt.Println(joined)
// 두 오류가 각각 한 줄씩
// 어떤 오류든 판정:
// errors.Is(joined, err1) // true
// 전부 nil 이면 nil 반환:
// errors.Join(nil, nil) // nil
// 작업 오류 수집:
func runAll() error {
var errs []error
if err := step1(); err != nil {
errs = append(errs, err)
}
if err := step2(); err != nil {
errs = append(errs, err)
}
return errors.Join(errs...)
}
// Go 1.20+
// 오류는 여러 타깃을 동시에 is 할 수 있음

12.입출력

파일 I/O, bufio, JSON, io.Reader/Writer를 다룹니다.

파일 읽기

작은 파일은 os.ReadFile로 통째로 읽습니다. 큰 파일은 os.Open + Reader(bufio.Scanner 등)로 스트리밍합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import "os"
// 파일 전체:
data, err := os.ReadFile("data.txt")
if err != nil {
log.Fatal(err)
}
// 한 줄씩 읽기:
f, err := os.Open("data.txt")
if err != nil {
log.Fatal(err)
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
fmt.Println(line)
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
// 큰 파일을 한 줄씩 읽으면 메모리를 차지하지 않음
// 청크 읽기:
// io.Reader + Read 로 고정 버퍼 사용

파일 쓰기

os.WriteFile로 통째로 쓰고, os.OpenFile로 이어 붙이며, bufio.Writer로 버퍼링된 쓰기를 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import "os"
// 일괄 쓰기:
err := os.WriteFile("out.txt",
[]byte("hello"), 0644)
// 추가:
f, _ := os.OpenFile("log.txt",
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
defer f.Close()
f.WriteString("更多内容\n")
// 버퍼 쓰기:
w := bufio.NewWriter(f)
w.WriteString("text")
w.Flush() // 파일로 플러시
// 권한:
// 0644 소유자는 읽기/쓰기、다른 사람은 읽기
// 디렉터리 생성:os.MkdirAll
// 오류 처리를 무시하지 말 것

JSON 직렬화

json.Marshal로 구조체를 JSON으로 인코딩합니다. 태그로 키 이름과 옵션을 제어합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import "encoding/json"
type User struct {
Name string `json:"name"`
Age int `json:"age"`
// 빈 값 무시:
Email string `json:"email,omitempty"`
// 필드 무시:
Secret string `json:"-"`
}
// 직렬화:
u := User{Name: "Nick", Age: 30}
data, err := json.Marshal(u)
// 정렬:
pretty, _ := json.MarshalIndent(u, "", " ")
// map 으로 변환:
m := map[string]any{"k": "v"}
b, _ := json.Marshal(m)
// 기본 필드명:
// 내보낸 필드명의 첫 글자가 소문자가 되나요? 아니요, 원래 이름 유지
// 태그로 출력 필드명 제어

JSON 역직렬화

json.Unmarshal로 JSON을 구조체나 맵으로 디코딩합니다. 큰 데이터는 json.NewDecoder로 스트리밍합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import "encoding/json"
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
// 문자열 파싱:
var u User
err := json.Unmarshal([]byte(`{"name":"Nick","age":30}`), &u)
// map 으로 파싱:
var m map[string]any
json.Unmarshal(data, &m)
// map 안의 숫자는 float64:
age := m["age"].(float64)
// 스트림 파싱(큰 JSON):
dec := json.NewDecoder(bytes.NewReader(data))
dec.Decode(&u)
// 알 수 없는 필드 무시:
// Decoder.DisallowUnknownFields() 는 그 반대
// 오류 처리:
// 구문 오류는 *json.SyntaxError 반환

io.Reader / Writer

io.Reader/Writer는 스트림을 추상화합니다. io.ReadAll과 io.Copy가 자주 쓰이는 헬퍼입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import (
"io"
"strings"
)
// Reader 에서 전부 읽기:
r := strings.NewReader("hello")
data, _ := io.ReadAll(r)
// 스트림 복사:
// io.Copy(dst, src)
// 스트림 파이프:
// io.Pipe()
// 조합:
// bufio.NewReader / bufio.NewWriter
// 제한 읽기:
// io.LimitReader(r, 100)
// 여러 리더:
// io.MultiReader(a, b)
// 여러 라이터:
// io.MultiWriter(a, b)
// 빈 구현:
// io.Discard 는 쓰기를 폐기

bufio 버퍼링

bufio는 버퍼링으로 시스템 콜을 줄입니다. Scanner로 줄 단위 처리가 쉽습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import (
"bufio"
"os"
)
// 읽기:
reader := bufio.NewReader(os.Stdin)
line, err := reader.ReadString('\n')
// 고정 바이트 읽기:
buf := make([]byte, 1024)
reader.Read(buf)
// Scanner 로 한 줄씩:
sc := bufio.NewScanner(os.Stdin)
for sc.Scan() {
fmt.Println(sc.Text())
}
// 쓰기:
w := bufio.NewWriter(os.Stdout)
w.WriteString("hello\n")
w.Flush()
// 버퍼 크기:
// bufio.NewReaderSize(r, 64*1024)
// Scanner 기본 행 상한은 64K:
// sc.Buffer(buf, max)
// 성능:시스템 콜 감소

표준 스트림

os.Stdin/Stdout/Stderr가 표준 스트림입니다. fmt.Scan과 bufio.Scanner로 입력을 처리합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import (
"bufio"
"fmt"
"os"
)
// 출력:
fmt.Println("hello")
// stderr 로:
fmt.Fprintln(os.Stderr, "error")
// 입력 읽기:
var name string
fmt.Scanln(&name)
// 포맷 스캔:
var age int
fmt.Scanf("%d", &age)
// stdin 을 한 줄씩 처리:
sc := bufio.NewScanner(os.Stdin)
for sc.Scan() {
fmt.Println(sc.Text())
}
// 파이프 사용법:
// cat data | go run app.go
// 출력 리다이렉트:
// go run app.go > out.txt

경로 조작

filepath로 플랫폼 독립적인 경로 처리를 합니다. Join, Dir, Base, Ext를 자주 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import (
"path/filepath"
"fmt"
)
// 경로 결합:
path := filepath.Join("dir", "sub", "a.txt")
// 디렉터리/파일명/확장자:
filepath.Dir(path) // dir/sub
filepath.Base(path) // a.txt
filepath.Ext(path) // .txt
// 불필요한 구분자 정리:
filepath.Clean("dir//./a.txt")
// 절대 경로:
abs, _ := filepath.Abs(path)
// 상대 경로:
rel, _ := filepath.Rel("/a", "/a/b/c")
// 구분자:
// Windows \\ Unix /
// filepath.Separator
// 매칭 순회:
// filepath.Glob("data/*.json")
// 재귀 순회:filepath.WalkDir
// 구분자를 하드코딩하지 말 것

13.흔한 함정

Go 일상 개발에서 가장 자주 만나는 함정과 올바른 관용구를 소개합니다.

nil과 빈 슬라이스

nil 슬라이스와 빈 슬라이스는 다릅니다. JSON 직렬화 시 nil은 null, 빈 슬라이스는 []가 됩니다. 의도를 명확히 하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// BAD:nil slice 는 null 로 직렬화
var s []string
json.Marshal(s) // "null"
// GOOD:빈 slice 는 [] 로 직렬화
s := make([]string, 0)
json.Marshal(s) // "[]"
// nil slice 는 append 가능:
var n []int
n = append(n, 1) // 정상
// 판정:
// len(s) == 0 은 둘 다 성립
// 컬렉션 반환 관례:
// 명시적으로 빈 slice 반환:
func list() []string {
return make([]string, 0)
}

루프 변수 캡처

Go 1.22 이전에는 루프 변수를 여러 goroutine이 공유했지만, 1.22부터는 매 반복마다 독립 변수가 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// BAD(1.22 이전):goroutine 이 모두 마지막 값을 출력
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i) // 모두 3 일 가능성
}()
}
// GOOD:명시적으로 인자 전달
for i := 0; i < 3; i++ {
go func(n int) {
fmt.Println(n)
}(i)
}
// 1.22+ 는 루프 변수가 매번 새 변수:
// for i := range 3 { go ... } 안전
// 구버전과 호환되는 작성법:
// 지역 복사 x := i
// 인자 전달이 가장 명확

map 순회 순서는 무작위

맵의 순회 순서는 무작위입니다. 안정적인 순서가 필요하면 키를 정렬해 순회하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// BAD:map 순회 순서에 의존
m := map[string]int{"a": 1, "b": 2}
for k, v := range m {
fmt.Println(k, v) // 순서 무작위
}
// GOOD:키 정렬 순회
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, m[k])
}
// 원인:map 해시 구현은 순서 없음
// 출력/JSON/테스트는 안정된 순서 필요
// slice 순회는 삽입 순서 유지

문자열 변경

문자열은 불변이므로 직접 수정할 수 없습니다. ASCII는 []byte로, 텍스트는 []rune으로 변환해 수정한 뒤 다시 문자열로 돌립니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// BAD:문자열은 직접 변경 불가
s := "hello"
// s[0] = 'H' 오류:cannot assign
// GOOD:가변 타입으로 변환
bs := []byte(s)
bs[0] = 'H'
s = string(bs) // "Hello"
// 중국어는 []rune 필요:
rs := []rune("你好")
rs[0] = '再'
s = string(rs) // "再好"
// 연결:
// + 또는 strings.Builder
// 문자열 변환에는 복사 비용이 있음
// 큰 문자열은 메모리 주의

슬라이스 복사와 공유

슬라이스를 대입하면 내부 배열을 공유합니다. 독립된 복사본은 copy로 만듭니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// BAD:대입이 데이터를 복사했다고 오해
original := []int{1, 2, 3}
copySlice := original
copySlice[0] = 99
fmt.Println(original) // [99 2 3] !
// GOOD:copy 로 복사
// copy 대상은 충분한 용량 필요
dst := make([]int, len(original))
copy(dst, original)
// 또는 append 패턴:
copySlice := append([]int{}, original...)
// append 는 공유할 수 있음:
// 확장 시에만 복사
// 함수가 반환하는 slice 는 공유 주의
// 변경 전에 소유권 확인

오류 무시

err을 무시하면 조용히 실패합니다. 반드시 처리하거나 로그를 남기거나, _에 대입해 의도를 보이세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// BAD:오류 무시
f, _ := os.Open("data.txt") // 실패하면 f 는 nil
f.Read(...) // 빈 포인터 panic
// BAD:맨 무시
json.Unmarshal(data, &u) // 검사 안 함
// GOOD:검사하고 처리
f, err := os.Open("data.txt")
if err != nil {
log.Fatalf("打开失败: %v", err)
}
defer f.Close()
// 명시적으로 무시할 의도:
_, _ = fmt.Println("x") // 드묾
// 관례:
// 모든 err != nil 을 처리
// 컨텍스트를 기록한 뒤 반환

리시버 불일치

같은 타입에서 값 리시버와 포인터 리시버가 섞이면 인터페이스 구현 등에서 미묘한 차이가 생깁니다. 한 가지로 통일하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// BAD:리시버 혼용
func (c Counter) Get() int { return c.N }
func (c *Counter) Inc() { c.N++ } // 불일치
// 일부 메서드가 포인터로만 구현:
// 값 타입은 인터페이스를 충족하지 못함
// GOOD:포인터 리시버로 통일
func (c *Counter) Get() int { return c.N }
func (c *Counter) Inc() { c.N++ }
// 인터페이스 구현:
var _ CounterIface = &Counter{} // 포인터
// 컴파일 타임 단언:
// var _ Shape = (*Circle)(nil)
// 변경 가능한 객체는 포인터 리시버 통일
// 작고 불변한 객체는 값 리시버

변수 섀도잉

내부 스코프에서 :=를 쓰면 새 변수가 만들어져 바깥 변수를 섀도잉합니다. 필요하면 명시적으로 =를 쓰세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// BAD:err 가 가려짐
func f() error {
x, err := compute()
if err != nil {
return err
}
if y, err := other(); err != nil {
// err 는 안쪽의 새 변수
return err
}
_ = x
return nil
}
// 가림으로 바깥 err 가 미대입
// 검출:
// go vet 가 shadow 를 보고
// 또는 -gcflags
// GOOD:err 재사용:
// y, err := other() 선언돼 있으면 재사용 가능
// 규칙:
// 안쪽에서 바깥 변수 변경 시 = 사용
// 새 변수는 := 사용

루프 안의 defer

루프 안의 defer는 바깥 함수가 끝날 때까지 실행되지 않습니다. 각 반복을 함수로 감싸 자원을 즉시 해제하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// BAD:반복문 안에서 defer 가 쌓인다
for _, f := range files {
fh, err := os.Open(f)
if err != nil {
continue
}
defer fh.Close() // 모두 함수 종료까지
}
// 대량의 파일을 열어둔 채 닫지 않는다
// GOOD:함수로 래핑
for _, f := range files {
err := processFile(f)
if err != nil {
log.Println(err)
}
}
func processFile(name string) error {
fh, err := os.Open(name)
if err != nil {
return err
}
defer fh.Close() // 호출마다 종료 시 닫는다
return nil
}
// 또는 즉시 close

14.동시성

goroutine, 채널, select, 동시성 안전을 다룹니다.

goroutine

go 키워드로 동시 함수를 시작합니다. goroutine은 가볍고 스택이 동적으로 자랍니다. main이 끝나면 프로그램도 끝납니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// goroutine 실행:
go fmt.Println("并发执行")
// 익명 함수:
go func() {
fmt.Println("后台任务")
}()
// main 함수는 goroutine 을 기다리지 않는다:
// 프로그램이 종료되면 모든 goroutine 도 종료
// 완료를 기다리려면 WaitGroup 이나 channel
// 예:
go func(n int) {
fmt.Println(n * 2)
}(10)
// 주의:
// goroutine 실행 순서에 의존하지 않기
// 에러 처리는 goroutine 내부에서

channel

채널은 goroutine 간 통신 채널입니다. 버퍼가 없으면 송수신이 서로를 기다리며 동기적으로 블록합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// channel 생성:
ch := make(chan int)
// 전송:
ch <- 42
// 수신:
value := <-ch
// 버퍼 없는 동기화:
// 송수신 양쪽이 짝이 될 때까지 블로킹
// channel 닫기:
close(ch)
// 수신으로 닫혔는지 판단:
v, ok := <-ch
if !ok {
fmt.Println("已关闭")
}
// 읽기 전용/쓰기 전용:
// func f(ch <-chan int) // 읽기 전용
// func g(ch chan<- int) // 쓰기 전용
// 단방향 channel 은 용도를 제한

버퍼 채널

make(chan T, n)은 버퍼 채널을 만듭니다. 가득 차면 송신이, 비면 수신이 블록합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 버퍼 2:
ch := make(chan int, 2)
ch <- 1 // 블로킹하지 않음
ch <- 2 // 블로킹하지 않음
// ch <- 3 // 버퍼가 가득 차면 블로킹
// 꺼내서 공간 확보:
<-ch
// 용도:
// 작업 큐:
work := make(chan Task, 10)
// 세마포어로 트래픽 제한:
// make(chan struct{}, 5)
// 생산자-소비자 분리
// 주의점:
// 버퍼는 블로킹 시점에만 영향
// 닫힌 뒤 전송은 panic
// 버퍼 안의 값을 꺼낸 뒤 닫힘을 확인

select

select는 여러 채널 연산을 동시에 기다립니다. 준비된 case가 여러 개면 무작위로 하나를 선택하고 default는 즉시 실행됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
select {
case v := <-ch1:
fmt.Println("ch1", v)
case v := <-ch2:
fmt.Println("ch2", v)
case ch3 <- 1:
fmt.Println("发送成功")
default:
fmt.Println("无就绪,非阻塞")
}
// 여러 개가 준비되면 무작위로 하나 선택
// 빈 select 는 영원히 블로킹:
// select {}
// 타임아웃 패턴:
select {
case v := <-data:
fmt.Println(v)
case <-time.After(2 * time.Second):
fmt.Println("超时")
}
// 반복문에서 select 로 여러 스트림 처리

WaitGroup

sync.WaitGroup으로 goroutine 묶음의 완료를 기다립니다. Add로 시작, defer Done으로 종료, Wait로 블록.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "sync"
var wg sync.WaitGroup
// 작업 실행:
for i := 0; i < 5; i++ {
wg.Add(1) // 카운트 +1
go func(n int) {
defer wg.Done() // 완료 -1
fmt.Println(n)
}(i)
}
// 전부 완료될 때까지 대기:
wg.Wait()
// 규칙:
// Add 는 goroutine 앞에서 호출
// Done 은 defer 로 실행 보장
// Wait 이후 카운트 재사용 불가
// 대량 작업은 반복문 밖에서 Add(n)
// Add 후 WaitGroup 복사하지 않기

뮤텍스

sync.Mutex는 Lock/Unlock으로 공유 상태를 보호합니다. 읽기가 많다면 sync.RWMutex가 유리합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import "sync"
var (
mu sync.Mutex
balance int
)
func Deposit(amount int) {
mu.Lock()
defer mu.Unlock() // 언락 보장
balance += amount
}
func Balance() int {
mu.Lock()
defer mu.Unlock()
return balance
}
// 읽기-쓰기 락:
// var rw sync.RWMutex
// rw.RLock() 읽기 락
// rw.RUnlock()
// 읽기는 병렬 가능, 쓰기는 배타
// 데드락 방지:
// 락 순서 통일
// 락을 쥔 채 시간이 걸리는 작업 하지 않기

sync.Once

sync.Once는 함수를 단 한 번만 실행하도록 보장합니다. 지연 싱글톤, 병렬 안전한 초기화에 적합합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import "sync"
var (
once sync.Once
instance *Config
)
// 싱글턴 패턴:
func GetConfig() *Config {
once.Do(func() {
instance = &Config{
Env: os.Getenv("APP_ENV"),
}
// 딱 한 번만 실행
})
return instance
}
// 특징:
// 여러 goroutine 에서 병행 안전
// 딱 한 번만 실행(성공/실패 여부와 무관)
// 용도:
// 데이터베이스 연결, 캐시 로드
// 레지스트리 초기화
// init 과의 차이:
// init 은 패키지 로드 시 반드시 실행
// Once 는 필요 시점에, 테스트에서 리셋 가능

데이터 경쟁 탐지

go run -race(또는 go test -race)로 동기화되지 않은 동시 접근을 탐지합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 검출:
// go run -race main.go
// go test -race ./...
// 레이스 예시:
var counter int
for i := 0; i < 1000; i++ {
go func() {
counter++ // 락 없음, 레이스
}()
}
// 출력:
// WARNING: DATA RACE
// 수정:
// 락, channel, 또는 atomic 사용:
var c atomic.Int64
c.Add(1)
// 모범 사례:
// 테스트에서는 항상 -race 활성화
// CI 에 race 검사 통합
// 값을 공유하지 말고 메시지 전달 사용

15.네트워크

HTTP 요청, 서버, context, TCP를 다룹니다.

HTTP GET

http.Get로 GET 요청을 보내고 defer로 resp.Body를 닫으며 상태 코드를 확인하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "net/http"
resp, err := http.Get("https://api.example.com/users")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close() // 반드시 닫기
// 상태 코드 확인:
if resp.StatusCode != http.StatusOK {
log.Fatalf("状态码 %d", resp.StatusCode)
}
// 응답 본문 읽기:
body, err := io.ReadAll(resp.Body)
fmt.Println(string(body))
// 응답 헤더:
contentType := resp.Header.Get("Content-Type")
// 주의:
// 기본은 타임아웃 없음, http.Client 사용
// 큰 응답은 읽는 양 제한

HTTP 서버

http.HandleFunc로 핸들러를 등록하고 ListenAndServe로 대기합니다. *http.Server로 graceful shutdown을 구현하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.URL.Path)
}
func main() {
http.HandleFunc("/", handler)
http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"ok": true}`)
})
// 8080 포트에서 대기:
log.Fatal(http.ListenAndServe(":8080", nil))
}
// 폼 읽기:r.FormValue("name")
// JSON 반환:json.NewEncoder(w).Encode(v)
// 정상 종료:http.Server + Shutdown

HTTP 클라이언트

http.Client로 타임아웃과 연결 풀을 설정하고, http.NewRequest로 메서드와 헤더, 본문을 자유롭게 구성합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import (
"net/http"
"time"
)
// 타임아웃 있는 클라이언트:
client := &http.Client{
Timeout: 10 * time.Second,
}
// 커스텀 요청:
req, _ := http.NewRequest("POST",
"https://api.example.com",
strings.NewReader(`{"name":"go"}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer token")
// 전송:
resp, err := client.Do(req)
// 커넥션 풀 재사용:
// 전역에서 하나의 Client 공유
// Transport 설정:
// MaxIdleConnsPerHost
// 타임아웃 종류:
// Timeout 은 응답 읽기를 포함

JSON API

JSON API 응답을 구조체에 디코딩하고, 태그로 필드 이름을 매핑합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func fetchUsers() ([]User, error) {
resp, err := http.Get("https://api.example.com/users")
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("状态码 %d", resp.StatusCode)
}
var users []User
if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
return nil, err
}
return users, nil
}
// 큰 응답은 Decoder 로 스트림 디코딩
// 알 수 없는 필드는 기본적으로 무시
// 서버가 반환하는 필드명은 태그와 일치

context

context는 취소와 데드라인을 전달합니다. WithTimeout/WithCancel로 자식 context를 만들고 I/O에 흘려보내세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "context"
// 타임아웃 설정:
ctx, cancel := context.WithTimeout(
context.Background(), 2*time.Second)
defer cancel() // 리소스 해제
// context 가 있는 요청 발행:
req, _ := http.NewRequestWithContext(
ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
// 취소 가능:
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(100 * time.Millisecond)
cancel() // 모든 하위 작업 취소
}()
// 전파:
// context 는 함수의 첫 번째 인자로
// 구조체 필드에 저장하지 않기
// 파생:WithValue 로 요청 메타데이터 전달

URL 파싱

url.Parse로 URL을 분해하고 url.Values.Encode로 쿼리 문자열을 만듭니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import "net/url"
u, err := url.Parse(
"https://user:[email protected]/path?q=go#sec")
// 각 세그먼트:
u.Scheme // "https"
u.Host // "example.com"
u.Path // "/path"
u.Fragment // "sec"
// 쿼리 파라미터:
q := u.Query()
q.Get("q") // "go"
// URL 구성:
base, _ := url.Parse("https://api.com")
base.Path = "/v1/users"
params := url.Values{}
params.Set("page", "2")
base.RawQuery = params.Encode()
// 인코딩:
// url.QueryEscape / PathEscape
// 결합은 ResolveReference 사용

TCP 소켓

net.Dial로 클라이언트 연결을 맺고 net.Listen으로 서버를 대기시킵니다. 연결은 바이트 스트림입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import (
"net"
"bufio"
)
// 클라이언트:
conn, err := net.Dial("tcp", "example.com:80")
defer conn.Close()
// HTTP 요청 전송:
fmt.Fprintf(conn,
"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")
// 응답 읽기:
resp, _ := bufio.NewReader(conn)
.ReadString('\n')
fmt.Println(resp)
// 서버:
ln, _ := net.Listen("tcp", ":8080")
for {
conn, _ := ln.Accept()
go handleConn(conn) // 연결마다 goroutine
}
// 쓰기:conn.Write([]byte)
// 타임아웃 설정:conn.SetDeadline

라우터 강화

Go 1.22 이후 http.ServeMux는 메서드와 경로 매개변수({id})를 지원합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "net/http"
// Go 1.22+ 라우팅:
mux := http.NewServeMux()
// 메서드 + 경로:
mux.HandleFunc("GET /users", listUsers)
mux.HandleFunc("POST /users", createUser)
// 경로 파라미터:
mux.HandleFunc("GET /users/{id}", getUser)
// 와일드카드:
mux.HandleFunc("/files/{path...}", serveFile)
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // 파라미터 조회
fmt.Fprintf(w, "用户 %s", id)
}
// 옛 방식:
// http.HandleFunc("/users/", handler)
// 수동으로 r.URL.Path 파싱
// 메서드 불일치 시 405 반환

16.시간과 날짜

time 패키지, Duration, 포맷팅, 시간대를 다룹니다.

현재 시각 얻기

time.Now로 현재 시각을 얻고 .UTC() / .In(loc)으로 시간대를 변환할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "time"
now := time.Now() // 로컬 시간
// 각 필드:
now.Year() // 2026
now.Month() // August
now.Day() // 2
now.Hour()
now.Minute()
now.Second()
now.Weekday() // Sunday
// UTC:
t := time.Now().UTC()
// Unix 타임스탬프:
unix := time.Now().Unix() // 초
milli := time.Now().UnixMilli() // 밀리초
// 특정 시각:
// time.Date(2026, 8, 2, 10, 0, 0, 0, time.UTC)
// 비교:t.Before(u) / t.After(u)

Duration

time.Duration은 나노초 단위의 정수입니다. time.Second 같은 상수와 함께 사용하고 더하기/빼기/슬립에 활용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import "time"
// 상수:
// time.Nanosecond
// time.Microsecond
// time.Millisecond
// time.Second
// time.Minute
// time.Hour
// 기간 정의:
d := 2 * time.Second
// 변환:
ms := d.Milliseconds()
ns := d.Nanoseconds()
// 포맷:d.String() // "2s"
// 파싱:
d, _ = time.ParseDuration("1h30m")
// 계산:
// t.Add(d) / t.Sub(u)
// t.AddDate(0, 1, 0) 1개월 더하기
// 대기:
// time.Sleep(d)
// 비교:
// d > time.Minute
// 타입 안전:int 로 직접 연산 불가

포맷팅

Time.Format은 참조 시각 2006-01-02 15:04:05를 사용해 포맷 문자열을 기술합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import "time"
t := time.Now()
// 표준 포맷:
t.Format("2006-01-02") // 2026-08-02
t.Format("15:04:05") // 14:30:22
t.Format("2006-01-02 15:04") // 전체 형태
t.Format("2006-01-02 15:04:05 MST") // 타임존 포함
// 참조 시각은 고정:
// 01月 02日 03时 04分 05秒
// 06年 07月 MST
// 2006 년이 핵심 표식
// 사전 정의:
// time.RFC3339:"2006-01-02T15:04:05Z07:00"
// time.RFC1123 / time.Kitchen
// 레이아웃이 틀리면 빈 문자열 반환

파싱

time.Parse는 같은 참조 레이아웃으로 문자열을 Time으로 변환합니다. 시간대가 있으면 ParseInLocation을 사용하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import "time"
// 레이아웃으로 파싱:
t, err := time.Parse("2006-01-02", "2026-08-02")
// 타임존 포함:
// time.Parse(time.RFC3339, "2026-08-02T10:00:00Z")
// 로컬 타임존으로 파싱:
// time.ParseInLocation(layout, value, time.Local)
// 흔한 오류:
// 레이아웃이 틀리면 제로 값 반환
// 월/일은 참조 값 사용
// 사용법:
// time.Parse("2006/01/02 15:04", "2026/08/02 10:30")
// 타임존 처리:
// 타임존 없으면 UTC 로 파싱
// 로컬 타임존 필요하면 ParseInLocation

Ticker

time.Ticker는 일정 간격으로 신호를 보냅니다. 사용이 끝나면 defer ticker.Stop()으로 자원을 해제하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import "time"
// 매초 트리거:
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop() // 해제
// 주기 실행:
for range ticker.C {
fmt.Println("tick", time.Now())
}
// 수동 제어:
// 한 번만 소비:
// <-ticker.C
// 중지 확인:
for {
select {
case t := <-ticker.C:
fmt.Println("执行", t)
case <-stop:
return // 종료
}
}
// 주의:
// Stop 을 잊으면 타이머 누수
// 버퍼 없는 C 는 늦으면 폐기
// 축약:time.Tick 는 해제 불가, 주의해서 사용

Sleep과 대기

time.Sleep은 현재 goroutine을 지정한 시간 동안 멈춥니다. 취소 가능한 대기는 Timer/After를 사용하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import "time"
// 2초 정지:
time.Sleep(2 * time.Second)
// 동적 기간:
wait := time.Duration(n) * time.Millisecond
time.Sleep(wait)
// 밀리초 값:
time.Sleep(500 * time.Millisecond)
// 주의:
// 현재 goroutine 을 블로킹
// 정밀한 스케줄링에는 부적합
// 테스트에서 가속:
// 기간을 변수로 추출해 교체 용이하게
// Timer 와의 차이:
// Sleep 은 무조건 블로킹
// Timer 는 취소 가능
// select 에서 Sleep 을 타임아웃으로 쓰지 않기

Timer

time.Timer는 지정 시간 후 한 번만 발화합니다. Stop/Reset으로 제어하고 select와 함께 취소도 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import "time"
// 3초 후 트리거:
timer := time.NewTimer(3 * time.Second)
defer timer.Stop()
// 트리거 대기:
<-timer.C
fmt.Println("定时到")
// 조기 취소:
// timer.Stop()
// 취소 여부 판단:
// select 사용:
select {
case <-timer.C:
fmt.Println("执行")
case <-ctx.Done():
fmt.Println("已取消")
}
// 리셋:
// timer.Reset(5 * time.Second)
// 일회성 vs Ticker 반복
// 용도:타임아웃 제어, 지연 작업
// After 는 Timer 의 편리한 래퍼

시간대

IANA 시간대는 time.LoadLocation으로 불러오고 .In(loc)으로 적용합니다. time.FixedZone으로 고정 오프셋도 만들 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "time"
// 타임존 로드:
loc, err := time.LoadLocation("Asia/Shanghai")
// 해당 타임존으로 변환:
t := time.Now().In(loc)
// 자주 쓰는 타임존:
// time.UTC、time.Local
// "America/New_York"
// "Europe/London"
// "Asia/Tokyo"
// 고정 오프셋:
fixed := time.FixedZone("CST", 8*3600)
// 특정 타임존으로 생성:
// time.Date(2026, 8, 2, 10, 0, 0, 0, loc)
// 주의:
// IANA 데이터베이스 없으면 오류
// Windows 는 tzdata 임포트 필요:
// import _ "time/tzdata"
// 시각은 UTC 로 저장 권장

17.프로세스와 환경

명령줄 인자, 환경 변수, 서브프로세스, 종료 코드를 다룹니다.

명령줄 인자

os.Args는 명령줄 인자의 슬라이스입니다. os.Args[0]은 프로그램 경로, 나머지가 사용자 인자입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import (
"fmt"
"os"
)
// 모든 인자:
// go run app.go a b c
// os.Args = ["app.go", "a", "b", "c"]
for i, arg := range os.Args {
fmt.Printf("args[%d] = %s\n", i, arg)
}
// 업무용 인자만:
args := os.Args[1:]
// 단순 파싱:
if len(args) < 1 {
fmt.Println("用法: app <name>")
os.Exit(1)
}
name := args[0]
// 주의:
// 공백으로 구분, 따옴표로 공백 포함 가능
// 복잡한 인자는 flag 패키지 사용
// 인자는 문자열

flag 파싱

flag 패키지로 명령줄 플래그를 파싱합니다. 기본값을 지정하고 Parse 후 값을 읽습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import "flag"
// 플래그 정의:
name := flag.String("name", "Go", "用户名")
count := flag.Int("n", 1, "次数")
verbose := flag.Bool("v", false, "详细输出")
// 파싱:
flag.Parse()
// 사용:
for i := 0; i < *count; i++ {
fmt.Printf("%s (%v)\n", *name, *verbose)
}
// 실행:
// go run app.go -name Nick -n 3 -v
// 플래그 외 인자:
rest := flag.Args()
// 커스텀 Usage:
flag.Usage = func() { fmt.Println("用法...") }
// 파싱 오류 확인:
// flag.Parse 는 오류 시 자동 종료
// 축약:
// flag.StringVar(&s, "x", "", "说明")

환경 변수

os.Getenv로 읽고 os.Setenv로 설정합니다. os.LookupEnv는 미설정과 빈 문자열을 구분합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import (
"fmt"
"os"
)
// 읽기:
path := os.Getenv("PATH")
// 기본값 지정:
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
// 존재 여부 판단:
val, ok := os.LookupEnv("APP_ENV")
if !ok {
fmt.Println("未设置 APP_ENV")
}
// 설정:
os.Setenv("APP_ENV", "production")
// 삭제:
os.Unsetenv("APP_ENV")
// 전체 나열:
// os.Environ() // ["KEY=value", ...]
// 확장:os.ExpandEnv("$HOME/x")
// 설정은 환경 변수 우선

종료 코드

os.Exit은 지정한 종료 코드로 즉시 종료합니다. 0이 성공, 그 외는 실패입니다. defer는 실행되지 않습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import "os"
// 성공 종료:
// 정상 return 의 종료 코드는 0
// 실패 종료:
if err != nil {
fmt.Fprintln(os.Stderr, "失败:", err)
os.Exit(1)
}
// 주의:
// os.Exit 는 defer 를 실행하지 않음
// 버퍼 출력을 플러시하지 않음
// 관례:
// 0 성공
// 1 일반 오류
// 2 사용법 오류
// 64-113 예약/시그널
// 라이브러리 코드에서 os.Exit 호출 금지
// main 에서만 사용
// log.Fatal 도 종료한다:
// log.Fatalf("错误: %v", err) // 退出码 1

서브프로세스 실행

exec.Command로 외부 명령을 실행하고, stdout/stderr를 캡처하거나 stdin을 연결하거나 환경 변수를 설정할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import (
"os/exec"
"strings"
)
// 실행 후 출력 얻기:
out, err := exec.Command("ls", "-l").Output()
if err != nil {
log.Fatal(err)
}
fmt.Println(string(out))
// 결합 출력(stderr 포함):
// cmd.CombinedOutput()
// stderr 캡처:
var stderr strings.Builder
cmd := exec.Command("go", "version")
cmd.Stderr = &stderr
// 환경 설정:
cmd.Env = append(os.Environ(), "FOO=bar")
// 대화형 입력:
// cmd.Stdin = strings.NewReader("data")
// 스트림 출력:
// cmd.Stdout = os.Stdout
// 실행 상태:cmd.Run()

시그널 처리

signal.Notify로 OS 시그널을 채널로 받아 처리합니다. SIGINT/SIGTERM을 잡아 graceful shutdown을 구현하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import (
"os"
"os/signal"
"syscall"
)
// 시그널 감시:
quit := make(chan os.Signal, 1)
signal.Notify(quit,
syscall.SIGINT, // Ctrl+C
syscall.SIGTERM) // kill
// 시그널 대기:
<-quit
fmt.Println("收到退出信号,清理资源")
// 상태 저장, 연결 종료, 작업 중지
os.Exit(0)
// 무시만:
// signal.Ignore(syscall.SIGINT)
// 감시 중지:
// signal.Stop(quit)
// 흔한 용도:
// 서비스 정상 종료, CLI 도구 중단

파일 시스템

MkdirAll로 디렉터리 생성, RemoveAll로 트리 삭제, Rename으로 이동, Stat으로 정보를 얻습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import "os"
// 디렉터리 생성(부모 포함):
os.MkdirAll("data/sub", 0755)
// 디렉터리 트리 삭제:
os.RemoveAll("data")
// 파일 삭제:
os.Remove("tmp.txt")
// 이름 변경/이동:
os.Rename("a.txt", "b.txt")
// 파일 정보:
info, err := os.Stat("data.txt")
if err == nil {
fmt.Println(info.Size())
fmt.Println(info.IsDir())
fmt.Println(info.ModTime())
}
// 존재 여부:
if _, err := os.Stat("f"); os.IsNotExist(err) {
fmt.Println("不存在")
}
// 임시 파일:
// os.CreateTemp("", "prefix-*")
// 디렉터리 순회:os.ReadDir("dir")

작업 디렉터리

os.Getwd로 현재 작업 디렉터리를 얻고, os.Chdir로 변경합니다. 절대 경로는 filepath.Abs를 사용하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import (
"os"
"path/filepath"
)
// 현재 디렉터리:
cwd, err := os.Getwd()
fmt.Println(cwd)
// 변경:
// os.Chdir("data")
// 경로 결합:
path := filepath.Join(cwd, "config", "app.yaml")
// 정리/해석:
// filepath.Clean(path)
// filepath.Abs(path)
// 자주 사용:
// 설정의 상대 경로 로드
// 캐시 디렉터리:
// os.UserCacheDir()
// 설정 디렉터리:
// os.UserConfigDir()
// 실행 파일 경로:
// os.Executable()
// 실행 디렉터리와 소스 디렉터리가 다를 수 있음

18.정규식

regexp 패키지: 컴파일, 매칭, 찾기, 치환, 캡처 그룹을 다룹니다.

컴파일

regexp.Compile은 오류를 반환하고 regexp.MustCompile은 실패 시 패닉합니다. 패키지 스코프 정의에 적합합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import "regexp"
// 컴파일(오류 반환 가능):
re, err := regexp.Compile(`^[a-z]+$`)
if err != nil {
log.Fatal(err)
}
// 고정 패턴은 바로 MustCompile:
re = regexp.MustCompile(`\d+`)
// 컴파일 비용이 높아 반복문 밖에서:
// 전역 변수:
var emailRe = regexp.MustCompile(
`^[\w.+-]+@[\w-]+\.[\w.]+$`)
// 축약:
// regexp.MatchString 은 매번 컴파일
// 자주 호출하면 미리 컴파일
// 컴파일 후 병행 안전하게 재사용 가능

매칭 판단

MatchString은 문자열 매칭, Match는 []byte 매칭을 판단합니다. 완전 일치에는 ^...$를 씁니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "regexp"
var digitRe = regexp.MustCompile(`\d+`)
// 문자열 판단:
digitRe.MatchString("abc123") // true
// 바이트 매치:
digitRe.Match([]byte("abc123")) // true
// 완전 일치:
// ^...$ 로 패턴 감싸기
re := regexp.MustCompile(`^\d{3}$`)
re.MatchString("123") // true
re.MatchString("1234") // false
// 대소문자:
// (?i) 인라인 플래그
// regexp.MustCompile(`(?i)^go$`)
// 설명:
// 기본은 탐욕 매치
// 빈 매치도 매치
// 용도:폼 검증, 로그 필터

찾기

FindString는 첫 매치를, FindStringIndex는 시작/끝 바이트 위치를 반환합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import "regexp"
var re = regexp.MustCompile(`\bgo\b`)
// 첫 번째 매치:
re.FindString("I love go and Go") // "go"
// 바이트:re.Find([]byte(s))
// 매치 위치:
idx := re.FindStringIndex("go and go")
// idx = [0 2]
// 모든 위치:
all := re.FindAllStringIndex("go and go", -1)
// [[0 2] [8 10]]
// 개수 제한:
// re.FindAllString(s, 2) 최대 2개
// 부분 매치 문자열:
// re.FindStringSubmatch(s)
// 매치 존재 여부 판단:
// re.FindString(s) != ""

모든 매치

FindAllString으로 모든 매치를 얻습니다. -1(또는 생략)은 무제한이고, 정수로 개수를 제한합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "regexp"
var re = regexp.MustCompile(`\d+`)
// 전체 매치:
all := re.FindAllString("a1 b22 c333", -1)
// ["1" "22" "333"]
// 처음 2개로 제한:
re.FindAllString(s, 2)
// 카운트:
// len(all)
// 바이트 버전:
// re.FindAll(s, -1)
// 위치:
// re.FindAllStringIndex(s, -1)
// 각 그룹의 매치:
// re.FindAllStringSubmatch(s, -1)
// 예:모든 이메일 추출
// 주의:
// 매치 없으면 nil 슬라이스 반환
// -1 은 개수 제한 없음

치환

ReplaceAllString로 모든 매치를 치환합니다. 캡처 그룹은 $1, $2로 참조할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "regexp"
var re = regexp.MustCompile(`\d+`)
// 모든 숫자 치환:
re.ReplaceAllString("a1b2", "#")
// "a#b#"
// 그룹 참조:
var nameRe = regexp.MustCompile(
`(\w+),\s*(\w+)`)
nameRe.ReplaceAllString("Doe, John", "$2 $1")
// "John Doe"
// 횟수 제한:
// re.ReplaceAllString(s, "x") 횟수 파라미터 없음
// ReplaceAllLiteralString 은 확장 안 함:
// re.ReplaceAllLiteralString(s, "$1")
// 콜백:
// re.ReplaceAllStringFunc(s, fn)
// 예:숫자에 1 더하기
// 매치 없으면 원본 문자열 반환

분할

re.Split로 정규식 패턴을 기준으로 문자열을 분할합니다. -1은 무제한입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "regexp"
// 쉼표로 분할:
var re = regexp.MustCompile(`,`)
parts := re.Split("a,b,c", -1)
// ["a" "b" "c"]
// 공백으로 분할:
spaces := regexp.MustCompile(`\s+`)
spaces.Split("a b\tc", -1)
// ["a" "b" "c"]
// 개수 제한:
re.Split(s, 2) // 최대 2조각
// 주의:
// -1 은 제한 없음
// 빈 매치 분할은 특수한 동작
// 대안:
// strings.Fields 는 공백 기준
// strings.Split 은 리터럴 기준
// 복잡한 패턴일 때만 정규식 사용
// 분할 결과의 빈 조각은 직접 필터

캡처 그룹

괄호는 서브매치를 캡처합니다. FindStringSubmatch로 얻고, (?P<name>) 형태의 이름 있는 그룹도 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "regexp"
// 캡처 그룹:
re := regexp.MustCompile(`(\d{4})-(\d{2})-(\d{2})`)
sub := re.FindStringSubmatch("2026-08-02")
// sub[0] = "2026-08-02" 전체
// sub[1] = "2026"
// sub[2] = "08"
// sub[3] = "02"
// 이름 있는 그룹:
nameRe := regexp.MustCompile(`(?P<year>\d{4})`)
sub = nameRe.FindStringSubmatch("2026")
idx := nameRe.SubexpIndex("year") // 1
sub[idx] // "2026"
// 전체 그룹:
// re.FindAllStringSubmatch(s, -1)
// 비캡처 그룹:
// (?:...) 은 인덱스를 사용하지 않음
// 그룹 수 확인:re.NumSubexp()

흔한 패턴

자주 쓰는 정규식(메일, URL, IP, 전화번호)과 핵심 문법(\d, \w, \s, 수량자, ^, $)을 정리합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "regexp"
// 이메일:
regexp.MustCompile(`^[\w.+-]+@[\w-]+\.[\w.]+$`)
// URL:
regexp.MustCompile(`^https?://[\w.-]+`)
// 휴대폰 번호(중국):
regexp.MustCompile(`^1[3-9]\d{9}$`)
// 주민등록번호:
regexp.MustCompile(`^\d{17}[\dXx]$`)
// IP 주소:
regexp.MustCompile(`^(\d{1,3}\.){3}\d{1,3}$`)
// 문법 요점:
// \d 숫자 \w 단어 \s 공백
// + 1개 이상 * 0개 이상
// {2,4} 횟수 범위
// ^ 행 시작 $ 행 끝
// 탐욕에서 비탐욕으로:\d+? 에 ? 추가
// 반대:\D \W \S
// 문자 클래스:[a-zA-Z] [^0-9]

19.빌드와 테스트

빌드, 단위 테스트, 정적 분석, 크로스 플랫폼 컴파일을 다룹니다.

go build

go build로 패키지를 컴파일합니다. -o, -tags, -ldflags로 출력과 옵션을 제어할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 현재 디렉터리 빌드:
// go build
// 출력 경로 지정:
// go build -o bin/app .
// 여러 패키지:
// go build ./...
// 컴파일만 하고 출력 없음(검사):
// go build ./...
// 빌드 태그:
// go build -tags "jsoniter"
// 크기 줄이기:
// go build -ldflags "-s -w"
// 버전 정보 삽입:
// go build -ldflags "-X main.version=1.2.3"
// 크로스 컴파일은 다음 절
// 캐시:
// 증분 빌드는 build cache 사용
// 검증:
// go build && go vet

단위 테스트

go test가 *_test.go 파일을 실행합니다. t.Error/t.Errorf는 비치명, t.Fatal/t.Fataliff는 해당 테스트를 중단합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 테스트 파일:calc_test.go
package main
import "testing"
func TestAdd(t *testing.T) {
got := add(1, 2)
if got != 3 {
t.Errorf("add(1,2) = %d; want 3", got)
}
}
// 테이블 기반 테스트:
func TestAddTable(t *testing.T) {
cases := []struct{ a, b, want int }{
{1, 2, 3}, {0, 0, 0}, {-1, 1, 0},
}
for _, c := range cases {
if got := add(c.a, c.b); got != c.want {
t.Errorf("add(%d,%d)=%d; want %d",
c.a, c.b, got, c.want)
}
}
}
// 실행:
// go test // 현재 패키지
// go test ./... // 전체
// go test -v -run TestAdd
// 커버리지:go test -cover

벤치마크

go test -bench는 Benchmark 함수를 실행합니다. 프레임워크가 b.N회를 루프 돌려 연산당 비용을 측정합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import "testing"
func BenchmarkConcat(b *testing.B) {
for i := 0; i < b.N; i++ {
concat("a", "b") // 측정 대상 함수
}
}
// 실행:
// go test -bench=. -benchmem
// 출력:
// BenchmarkConcat-8 42984412
// 27.36 ns/op 16 B/op 2 allocs/op
// 옵션:
// -bench=Concat 이름으로 매치
// -benchmem 메모리 할당 표시
// -count=3 여러 번 실행
// -benchtime=5s 고정 시간
// 최적화 비교:
// 최적화 전후 각 1회 실행 후 비교
// 주의:
// 컴파일러가 인라인/상수 폴딩할 수 있음
// 워밍업이 눈에 띄지 않으면 무시 가능

정적 분석

go vet은 흔한 실수(printf 인자 불일치, 락 사용 오류 등)를 잡아냅니다. CI에서 반드시 실행하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 현재 패키지 검사:
// go vet
// 전체 검사:
// go vet ./...
// 흔한 검출:
// 1. 형식 문자열 오류:
// fmt.Printf("%d", "x") 타입 불일치
// 2. 락 사용 오류:
// sync.Mutex 복사
// 3. 무용한 대입
// 4. 문자열 이스케이프 문제
// 5. 오프셋/인덱스 범위 초과
// 다른 도구:
// golangci-lint 가 더 많은 검사 통합
// 정적 분석:
// go build -gcflags=-m 로 인라인 확인
// 모범 사례:
// 커밋 전에 반드시 실행
// CI 에 go vet ./... 통합

포맷팅

gofmt가 표준 포매터입니다. goimports를 함께 쓰면 임포트까지 자동 정리됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 단일 파일 포맷:
// gofmt -w file.go
// 전체 포맷:
// gofmt -w .
// 검사만 하고 변경 안 함:
// gofmt -l .
// 디렉터리 재귀:
// gofmt -l ./...
// import 정렬:
// goimports -w file.go
// 편집기 통합:
// 저장 시 자동 포맷
// 팀 협업:
// 통일된 포맷으로 스타일 논쟁 없음
// 주의:
// gofmt 는 포맷만 변경, 로직은 유지
// CI 검사:
// test -z "$(gofmt -l .)"
// 탭과 공백 혼용 자동 수정

크로스 컴파일

GOOS와 GOARCH를 지정해 go build를 실행하면 크로스 컴파일할 수 있습니다. CGO 사용 시에는 대상용 C 도구 체인이 필요합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Windows 빌드:
// GOOS=windows GOARCH=amd64 go build .
// Linux 빌드:
// GOOS=linux GOARCH=amd64 go build .
// macOS 빌드:
// GOOS=darwin GOARCH=arm64 go build .
// 32비트:
// GOOS=linux GOARCH=386
// ARM:
// GOOS=linux GOARCH=arm64
// 출력 이름에 접미사:
// go build -o app.exe .
// 사용 가능한 플랫폼 조합:
// go tool dist list
// 주의:
// CGO 기본 비활성(순수 Go 만 크로스 가능)
// 크로스 컴파일에 대상 환경 불필요
// 흔한 조합으로 릴리스 매트릭스

경쟁 탐지

go build -race(또는 go test -race)로는 런타임 중 데이터 경쟁을 보고하도록 바이너리를 계측합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 검출 포함 실행:
// go run -race main.go
// 검출 포함 테스트:
// go test -race ./...
// 검출 포함 빌드:
// go build -race
// 검출 내용:
// 여러 goroutine 이 동시에 읽기/쓰기
// 같은 메모리 주소
// 동기화 보호 없음
// 출력 예시:
// WARNING: DATA RACE
// Read at 0x... by goroutine 5
// 수정 방법:
// 뮤텍스 락 / channel 사용
// atomic 연산 사용
// 주의:
// 런타임 경로만 검출
// 커버리지는 테스트에 의존
// 프로덕션 바이너리에 -race 사용 금지
// CI 에서 항상 -race 활성화

모듈 버전

go.mod는 버전을 고정하고, go get은 업데이트, replace는 경로를 바꾸며, go.sum은 해시로 무결성을 검증합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 의존성 추가:
// go get github.com/user/[email protected]
// 최신으로 업데이트:
// go get -u ./...
// major 업그레이드:
// go get module/v2@latest
// replace 로 로컬 디버깅:
// replace github.com/foo/bar => ../bar
// 또는 fork 지정:
// replace foo => github.com/user/foo v1.0.0
// 미사용 제거:
// go mod tidy
// 체크섬:
// go mod verify
// 버전 고정:
// go.mod 의 require 블록 확인
// go.sum 이 무결성 보장:
// 의존성 해시 검증

CI 통합

CI는 포맷 검사, go vet, go test -race -cover, 빌드, 아티팩트 배포 순으로 구성하는 것이 일반적입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 전형적인 CI 단계:
// 1. 코드 체크아웃
// 2. Go 설치:
// actions/setup-go
// 3. 모듈 캐시:
// $HOME/go/pkg/mod
// 4. 포맷 검사:
// gofmt -l .
// 5. 정적 검사:
// go vet ./...
// 6. 테스트:
// go test -race -cover ./...
// 7. 빌드:
// go build ./...
// 8. 산출물 배포:
// 여러 플랫폼 바이너리 크로스 빌드
// 매트릭스:
// Go 버전 1.21 / 1.22
// OS linux/windows/darwin
// 기준:
// 테스트 실패 즉시 실패
// 커버리지 임계값은 선택

공식 링크

공식 문서와 리소스에 대한 직접 링크.

이 치트시트에 대해

이 페이지는 Go 1.22를 위한 자체 포함형 빠른 레퍼런스로, 언어 핵심과 실전 프로젝트에서 매일 쓰는 표준 라이브러리의 약 80%를 다룹니다. 짧은 변수 선언, 구조적 오류 처리(errors.Is / errors.As), 제네릭, 그리고 1.22부터 도입된 반복별 독립 루프 변수 같은 현대적 관용구를 중심으로 정리했습니다. 자세한 참고는 공식 Go 튜토리얼과 Effective Go를 보세요. 19개 섹션은 각각 하나의 주제에 집중합니다 — 첫 프로그램부터 goroutine, 인터페이스, 흔한 함정까지. 각 섹션은 8~14개의 짧은 소주제(각 5~20줄 코드)로 나뉘어 있으며, 총 150개 가량입니다. 코드 예제는 의도적으로 짧고 자기 설명적으로 작성되어 있습니다. 모든 처리는 브라우저 안에서 이루어집니다 — 업로드도 추적도 없습니다. 이 페이지는 GuruToolkit의 무료 개발자 도구 모음의 일부이며, 사용 가능한 코드는 자유롭게 활용할 수 있으나 어떠한 보증도 제공되지 않습니다.

버전 2.1.0