1. 왜 Go인가
Go(Golang)는 2009년 Google에서 만든 언어입니다. 설계 철학은 **단순함, 빠른 컴파일, 강력한 동시성**입니다.
Go가 선택받는 이유
- **Docker, Kubernetes, Terraform, Prometheus** 등 클라우드 인프라의 핵심 도구가 모두 Go로 작성되었습니다
- 컴파일 속도가 매우 빠릅니다. 수십만 줄 프로젝트도 몇 초 안에 빌드됩니다
- 정적 바이너리를 생성하므로 배포가 단순합니다. 별도 런타임이 필요 없습니다
- 고루틴을 통해 수십만 개의 동시 작업을 효율적으로 처리합니다
- 가비지 컬렉션이 있지만, 지연 시간이 매우 짧습니다
Go vs 다른 언어
| 항목 | Go | Python | Java | Rust |
|------|-----|--------|------|------|
| 컴파일 속도 | 매우 빠름 | 인터프리터 | 느림 | 느림 |
| 실행 속도 | 빠름 | 느림 | 빠름 | 매우 빠름 |
| 동시성 | 고루틴 | asyncio | 스레드 | async/tokio |
| 학습 곡선 | 낮음 | 매우 낮음 | 높음 | 매우 높음 |
| 메모리 관리 | GC | GC | GC | 소유권 |
Go는 **생산성과 성능의 최적 균형점**에 위치합니다. Rust만큼 빠르진 않지만, 팀 전체가 빠르게 배우고 유지보수할 수 있습니다.
2. 기본 문법
변수와 타입
package main
func main() {
// 명시적 선언
var name string = "Gopher"
var age int = 10
// 짧은 선언 (타입 추론)
language := "Go"
version := 1.22
// 상수
const pi = 3.14159
fmt.Printf("%s는 %d살이고 %s %.2f를 씁니다\n",
name, age, language, version)
fmt.Println("원주율:", pi)
}
함수
// 기본 함수
func add(a, b int) int {
return a + b
}
// 다중 반환값
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("0으로 나눌 수 없습니다")
}
return a / b, nil
}
// 명명된 반환값
func swap(a, b string) (first, second string) {
first = b
second = a
return // naked return
}
// 가변 인자
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
구조체
type User struct {
Name string
Email string
Age int
}
// 메서드 (값 리시버)
func (u User) String() string {
return fmt.Sprintf("%s (%s)", u.Name, u.Email)
}
// 메서드 (포인터 리시버 - 값 수정 가능)
func (u *User) SetEmail(email string) {
u.Email = email
}
func main() {
user := User{Name: "김영주", Email: "yj@example.com", Age: 30}
user.SetEmail("new@example.com")
fmt.Println(user) // 김영주 (new@example.com)
}
포인터
func main() {
x := 42
p := &x // x의 주소
fmt.Println(*p) // 42 (역참조)
*p = 100
fmt.Println(x) // 100
}
// Go에는 포인터 산술이 없습니다 - 안전합니다
// C와 달리 댕글링 포인터 걱정이 적습니다
슬라이스와 맵
func main() {
// 슬라이스
nums := []int{1, 2, 3, 4, 5}
nums = append(nums, 6, 7)
sub := nums[1:4] // [2, 3, 4]
// make로 생성
buffer := make([]byte, 0, 1024) // len=0, cap=1024
// 맵
scores := map[string]int{
"Alice": 95,
"Bob": 87,
}
scores["Charlie"] = 92
// 키 존재 확인
val, ok := scores["Dave"]
if !ok {
fmt.Println("Dave의 점수가 없습니다")
}
// 맵 순회
for name, score := range scores {
fmt.Printf("%s: %d\n", name, score)
}
}
3. 고루틴과 채널
Go의 동시성 모델은 **CSP(Communicating Sequential Processes)** 기반입니다. 핵심은 두 가지입니다: **고루틴**과 **채널**.
고루틴 기초
func worker(id int) {
fmt.Printf("Worker %d 시작\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d 완료\n", id)
}
func main() {
for i := 1; i <= 5; i++ {
go worker(i) // 고루틴 실행
}
// 메인 고루틴이 끝나면 프로그램 종료
time.Sleep(2 * time.Second)
}
고루틴은 **OS 스레드가 아닙니다**. Go 런타임이 수천 개의 고루틴을 소수의 OS 스레드에 **다중화(multiplexing)** 합니다. 고루틴 하나의 초기 스택은 약 2KB로, 수십만 개를 동시에 실행할 수 있습니다.
채널 통신
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i // 채널에 값 전송
fmt.Printf("전송: %d\n", i)
}
close(ch) // 채널 닫기
}
func consumer(ch <-chan int) {
for val := range ch { // 채널이 닫힐 때까지 수신
fmt.Printf("수신: %d\n", val)
}
}
func main() {
ch := make(chan int, 3) // 버퍼 크기 3
go producer(ch)
consumer(ch) // 메인 고루틴에서 소비
}
select 문
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "one"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "two"
}()
for i := 0; i < 2; i++ {
select {
case msg := <-ch1:
fmt.Println("ch1:", msg)
case msg := <-ch2:
fmt.Println("ch2:", msg)
case <-time.After(3 * time.Second):
fmt.Println("타임아웃!")
}
}
}
WaitGroup
func main() {
var wg sync.WaitGroup
urls := []string{
"https://example.com",
"https://golang.org",
"https://github.com",
}
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
resp, err := http.Get(u)
if err != nil {
fmt.Printf("에러: %s - %v\n", u, err)
return
}
defer resp.Body.Close()
fmt.Printf("%s -> %s\n", u, resp.Status)
}(url)
}
wg.Wait() // 모든 고루틴 완료 대기
fmt.Println("모든 요청 완료")
}
데드락 방지 패턴
// 나쁜 예: 데드락 발생
func bad() {
ch := make(chan int) // 버퍼 없는 채널
ch <- 1 // 수신자가 없어서 영원히 블록
fmt.Println(<-ch)
}
// 좋은 예 1: 고루틴으로 전송
func good1() {
ch := make(chan int)
go func() { ch <- 1 }()
fmt.Println(<-ch)
}
// 좋은 예 2: 버퍼 채널 사용
func good2() {
ch := make(chan int, 1)
ch <- 1
fmt.Println(<-ch)
}
워커 풀 패턴
func workerPool(jobs <-chan int, results chan<- int, id int) {
for j := range jobs {
fmt.Printf("Worker %d 처리 중: job %d\n", id, j)
time.Sleep(time.Millisecond * 500)
results <- j * 2
}
}
func main() {
const numJobs = 10
const numWorkers = 3
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
// 워커 시작
for w := 1; w <= numWorkers; w++ {
go workerPool(jobs, results, w)
}
// 작업 전송
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
// 결과 수집
for r := 1; r <= numJobs; r++ {
result := <-results
fmt.Printf("결과: %d\n", result)
}
}
4. 인터페이스
Go의 인터페이스는 **암시적으로 구현**됩니다. implements 키워드가 없습니다.
기본 인터페이스
type Shape interface {
Area() float64
Perimeter() float64
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
// Shape 인터페이스를 매개변수로 받는 함수
func printInfo(s Shape) {
fmt.Printf("면적: %.2f, 둘레: %.2f\n", s.Area(), s.Perimeter())
}
io.Reader / io.Writer
Go 표준 라이브러리의 가장 강력한 인터페이스입니다.
// io.Reader 인터페이스
// type Reader interface {
// Read(p []byte) (n int, err error)
// }
// io.Writer 인터페이스
// type Writer interface {
// Write(p []byte) (n int, err error)
// }
func countBytes(r io.Reader) (int, error) {
buf := make([]byte, 1024)
total := 0
for {
n, err := r.Read(buf)
total += n
if err == io.EOF {
return total, nil
}
if err != nil {
return total, err
}
}
}
func main() {
// 문자열에서 읽기
r := strings.NewReader("Hello, Go!")
n, _ := countBytes(r)
fmt.Printf("%d 바이트\n", n) // 10 바이트
// 파일에서 읽기
f, _ := os.Open("data.txt")
defer f.Close()
n, _ = countBytes(f)
fmt.Printf("%d 바이트\n", n)
}
빈 인터페이스와 타입 단언
func describe(i interface{}) {
// 타입 단언
if s, ok := i.(string); ok {
fmt.Println("문자열:", s)
return
}
// 타입 스위치
switch v := i.(type) {
case int:
fmt.Println("정수:", v)
case float64:
fmt.Println("실수:", v)
case bool:
fmt.Println("불린:", v)
default:
fmt.Printf("알 수 없는 타입: %T\n", v)
}
}
func main() {
describe(42)
describe("hello")
describe(3.14)
describe(true)
}
인터페이스 조합
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// 인터페이스 조합 (embedding)
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
5. 에러 처리
Go는 예외(exception) 대신 **명시적 에러 반환**을 사용합니다.
errors.New와 fmt.Errorf
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
func findUser(id int) (*User, error) {
if id <= 0 {
return nil, fmt.Errorf("잘못된 사용자 ID: %d", id)
}
// ... DB 조회
return nil, ErrNotFound
}
에러 래핑과 errors.Is / errors.As
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("유효성 검사 실패 - %s: %s", e.Field, e.Message)
}
func validateAge(age int) error {
if age < 0 || age > 150 {
return &ValidationError{
Field: "age",
Message: fmt.Sprintf("나이는 0-150 범위여야 합니다 (입력값: %d)", age),
}
}
return nil
}
func processUser(age int) error {
if err := validateAge(age); err != nil {
return fmt.Errorf("사용자 처리 실패: %w", err) // 에러 래핑
}
return nil
}
func main() {
err := processUser(-5)
// errors.Is: 에러 체인에서 특정 에러 확인
if errors.Is(err, ErrNotFound) {
fmt.Println("사용자를 찾을 수 없습니다")
}
// errors.As: 에러 체인에서 특정 타입으로 변환
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("필드: %s, 메시지: %s\n", valErr.Field, valErr.Message)
}
}
커스텀 에러 패턴
type AppError struct {
Code int
Message string
Err error
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error {
return e.Err
}
// 에러 생성 헬퍼
func NewAppError(code int, msg string, err error) *AppError {
return &AppError{Code: code, Message: msg, Err: err}
}
6. 제네릭 (Go 1.18+)
Go 1.18부터 제네릭이 도입되어 타입 안전한 범용 코드를 작성할 수 있습니다.
타입 파라미터
func Min[T constraints.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
func main() {
fmt.Println(Min(3, 5)) // int
fmt.Println(Min(3.14, 2.71)) // float64
fmt.Println(Min("a", "b")) // string
}
제약 조건
// 커스텀 제약 조건
type Number interface {
constraints.Integer | constraints.Float
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
// 구조체에 제네릭 적용
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) {
if len(s.items) == 0 {
var zero T
return zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
func (s *Stack[T]) Len() int {
return len(s.items)
}
실전 제네릭 예제: 맵 유틸리티
func Keys[K comparable, V any](m map[K]V) []K {
keys := make([]K, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}
func Values[K comparable, V any](m map[K]V) []V {
vals := make([]V, 0, len(m))
for _, v := range m {
vals = append(vals, v)
}
return vals
}
func Filter[T any](slice []T, predicate func(T) bool) []T {
var result []T
for _, item := range slice {
if predicate(item) {
result = append(result, item)
}
}
return result
}
func Map[T any, U any](slice []T, transform func(T) U) []U {
result := make([]U, len(slice))
for i, item := range slice {
result[i] = transform(item)
}
return result
}
7. 테스트
Go는 내장 테스트 프레임워크가 강력합니다. 별도 라이브러리 없이도 충분합니다.
기본 테스트
// math.go
package math
func Add(a, b int) int {
return a + b
}
func Fibonacci(n int) int {
if n <= 1 {
return n
}
return Fibonacci(n-1) + Fibonacci(n-2)
}
// math_test.go
package math
func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Add(2, 3) = %d; want 5", result)
}
}
테이블 드리븐 테스트
func TestFibonacci(t *testing.T) {
tests := []struct {
name string
input int
expected int
}{
{"zero", 0, 0},
{"one", 1, 1},
{"two", 2, 1},
{"five", 5, 5},
{"ten", 10, 55},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := Fibonacci(tc.input)
if result != tc.expected {
t.Errorf("Fibonacci(%d) = %d; want %d",
tc.input, result, tc.expected)
}
})
}
}
벤치마크
func BenchmarkFibonacci(b *testing.B) {
for i := 0; i < b.N; i++ {
Fibonacci(20)
}
}
// 실행: go test -bench=. -benchmem
// BenchmarkFibonacci-8 28735 41523 ns/op 0 B/op 0 allocs/op
httptest
func TestHealthHandler(t *testing.T) {
req := httptest.NewRequest("GET", "/health", nil)
w := httptest.NewRecorder()
healthHandler(w, req)
resp := w.Result()
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("상태 코드 = %d; want %d",
resp.StatusCode, http.StatusOK)
}
body, _ := io.ReadAll(resp.Body)
if string(body) != `{"status":"ok"}` {
t.Errorf("응답 = %s; want {\"status\":\"ok\"}", body)
}
}
8. 패키지와 모듈
go mod 초기화
새 프로젝트 시작
mkdir myproject && cd myproject
go mod init github.com/username/myproject
의존성 추가
go get github.com/gin-gonic/gin@latest
사용하지 않는 의존성 정리
go mod tidy
의존성 다운로드
go mod download
프로젝트 구조
myproject/
cmd/
server/
main.go # 엔트리포인트
internal/
handler/
user.go # HTTP 핸들러
user_test.go
service/
user.go # 비즈니스 로직
repository/
user.go # 데이터 접근
pkg/
validator/
validator.go # 외부 공개 유틸리티
go.mod
go.sum
internal 패키지
`internal` 디렉토리에 있는 패키지는 해당 모듈 외부에서 임포트할 수 없습니다. 이를 통해 공개 API와 내부 구현을 명확히 분리합니다.
// internal/config/config.go
package config
type Config struct {
Port int
DBHost string
LogLevel string
}
func Load() (*Config, error) {
// 환경 변수에서 설정 로드
return &Config{
Port: 8080,
DBHost: "localhost:5432",
LogLevel: "info",
}, nil
}
9. 실전 패턴
웹 서버 (net/http)
package main
"encoding/json"
"log"
"net/http"
"time"
)
type Response struct {
Message string `json:"message"`
Timestamp string `json:"timestamp"`
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(Response{
Message: "ok",
Timestamp: time.Now().Format(time.RFC3339),
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", healthHandler)
mux.HandleFunc("GET /api/users", getUsersHandler)
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
log.Printf("서버 시작: %s", server.Addr)
log.Fatal(server.ListenAndServe())
}
미들웨어
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("[%s] %s 시작", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
log.Printf("[%s] %s 완료 (%v)",
r.Method, r.URL.Path, time.Since(start))
})
}
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "인증 필요", http.StatusUnauthorized)
return
}
// 토큰 검증 로직...
next.ServeHTTP(w, r)
})
}
// 미들웨어 체이닝
func chain(h http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", healthHandler)
handler := chain(mux, loggingMiddleware, authMiddleware)
log.Fatal(http.ListenAndServe(":8080", handler))
}
Graceful Shutdown
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", healthHandler)
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
// 서버를 고루틴에서 시작
go func() {
log.Println("서버 시작: :8080")
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("서버 에러: %v", err)
}
}()
// 종료 시그널 대기
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("서버 종료 시작...")
// 30초 타임아웃으로 graceful shutdown
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("서버 강제 종료: %v", err)
}
log.Println("서버 종료 완료")
}
설정 관리
type Config struct {
Server ServerConfig
Database DatabaseConfig
}
type ServerConfig struct {
Port int `json:"port"`
ReadTimeout time.Duration `json:"read_timeout"`
WriteTimeout time.Duration `json:"write_timeout"`
}
type DatabaseConfig struct {
Host string `json:"host"`
Port int `json:"port"`
User string `json:"user"`
Password string `json:"password"`
DBName string `json:"dbname"`
}
func (d DatabaseConfig) DSN() string {
return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
d.Host, d.Port, d.User, d.Password, d.DBName)
}
func LoadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("설정 파일 읽기 실패: %w", err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("설정 파싱 실패: %w", err)
}
return &cfg, nil
}
10. Go in 2026
클라우드 인프라
Go는 클라우드 네이티브 생태계의 **사실상 표준 언어**입니다.
- **컨테이너 오케스트레이션**: Kubernetes, Docker, containerd
- **서비스 메시**: Istio, Linkerd
- **모니터링**: Prometheus, Grafana Agent, Thanos
- **IaC**: Terraform, Pulumi
- **CI/CD**: Drone, Tekton
CLI 도구
Go로 만든 CLI 도구는 단일 바이너리로 배포되어 설치가 간편합니다.
// cobra를 이용한 CLI 예제
package main
"fmt"
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "mytool",
Short: "나의 CLI 도구",
}
var greetCmd = &cobra.Command{
Use: "greet [name]",
Short: "인사하기",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("안녕하세요, %s!\n", args[0])
},
}
func main() {
rootCmd.AddCommand(greetCmd)
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
마이크로서비스
Go는 마이크로서비스 아키텍처에 최적화되어 있습니다.
- **빠른 시작 시간**: 콜드 스타트 지연 최소화
- **낮은 메모리 사용량**: 컨테이너 리소스 절약
- **정적 바이너리**: 경량 Docker 이미지 (scratch 베이스 가능)
- **gRPC 네이티브 지원**: protobuf + gRPC로 서비스 간 통신
- **OpenTelemetry**: 분산 추적 및 메트릭 수집
멀티 스테이지 빌드
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /server ./cmd/server
FROM scratch
COPY --from=builder /server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
최종 이미지 크기는 **10-20MB** 수준으로, Java나 Python 기반 서비스 대비 1/10 이하입니다.
마치며
Go는 **단순함이 곧 강력함**인 언어입니다. 복잡한 기능을 배제하고, 꼭 필요한 것만 제공합니다. 이 철학이 Docker, Kubernetes 같은 인프라 도구부터 CLI, 마이크로서비스까지 광범위한 영역에서 Go가 선택받는 이유입니다.
시작하기에 가장 좋은 방법은 직접 코드를 작성하는 것입니다. 간단한 CLI 도구나 REST API 서버를 만들어 보세요.
**Q1.** 고루틴과 OS 스레드의 차이는 무엇인가요?
Go 런타임이 고루틴을 소수의 OS 스레드에 다중화합니다. 고루틴의 초기 스택은 약 2KB로, OS 스레드의 1-8MB보다 훨씬 가볍습니다.
**Q2.** 버퍼 없는 채널에서 전송과 수신의 관계는?
전송자는 수신자가 준비될 때까지 블록되고, 수신자도 전송자가 준비될 때까지 블록됩니다. 동기적 통신 방식입니다.
**Q3.** select 문의 역할은?
여러 채널 연산을 동시에 대기하면서, 준비된 채널에서 먼저 처리합니다. 타임아웃이나 취소 처리에 유용합니다.
현재 단락 (1/744)
Go(Golang)는 2009년 Google에서 만든 언어입니다. 설계 철학은 **단순함, 빠른 컴파일, 강력한 동시성**입니다.