- 1. 왜 Go인가
- 2. 기본 문법
- 3. 고루틴과 채널
- 4. 인터페이스
- 5. 에러 처리
- 6. 제네릭 (Go 1.18+)
- 7. 테스트
- 8. 패키지와 모듈
- 9. 실전 패턴
- 10. Go in 2026
- 마치며
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
import "fmt"
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
import (
"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
}
제약 조건
import "golang.org/x/exp/constraints"
// 커스텀 제약 조건
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
import "testing"
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
import (
"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
import (
"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 /server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
최종 이미지 크기는 10-20MB 수준으로, Java나 Python 기반 서비스 대비 1/10 이하입니다.
마치며
Go는 단순함이 곧 강력함인 언어입니다. 복잡한 기능을 배제하고, 꼭 필요한 것만 제공합니다. 이 철학이 Docker, Kubernetes 같은 인프라 도구부터 CLI, 마이크로서비스까지 광범위한 영역에서 Go가 선택받는 이유입니다.
시작하기에 가장 좋은 방법은 직접 코드를 작성하는 것입니다. 간단한 CLI 도구나 REST API 서버를 만들어 보세요.
퀴즈: Go 동시성 이해도 점검
Q1. 고루틴과 OS 스레드의 차이는 무엇인가요?
Go 런타임이 고루틴을 소수의 OS 스레드에 다중화합니다. 고루틴의 초기 스택은 약 2KB로, OS 스레드의 1-8MB보다 훨씬 가볍습니다.
Q2. 버퍼 없는 채널에서 전송과 수신의 관계는?
전송자는 수신자가 준비될 때까지 블록되고, 수신자도 전송자가 준비될 때까지 블록됩니다. 동기적 통신 방식입니다.
Q3. select 문의 역할은?
여러 채널 연산을 동시에 대기하면서, 준비된 채널에서 먼저 처리합니다. 타임아웃이나 취소 처리에 유용합니다.
현재 단락 (1/747)
Go(Golang)는 2009년 Google에서 만든 언어입니다. 설계 철학은 **단순함, 빠른 컴파일, 강력한 동시성**입니다.