Split View: Go 언어 완전 가이드 2025: 고루틴, 채널, 인터페이스부터 프로덕션 패턴까지
Go 언어 완전 가이드 2025: 고루틴, 채널, 인터페이스부터 프로덕션 패턴까지
- 도입: 2025년, 왜 Go인가?
- 1. Go 기초: 타입, 구조체, 메서드, 포인터
- 2. 인터페이스와 컴포지션
- 3. 동시성: 고루틴과 채널
- 4. 동시성 패턴
- 5. 에러 처리
- 6. 제네릭 (Go 1.18+)
- 7. 테스트
- 8. REST API 구축
- 9. gRPC 서비스 구축
- 10. CLI 도구 개발
- 11. 프로덕션 배포
- 12. Go vs Rust vs Java 비교
- 13. 면접 질문 15선
- 14. 실전 퀴즈
- 참고 자료
도입: 2025년, 왜 Go인가?
Kubernetes, Docker, Terraform, Prometheus, Istio — 클라우드 네이티브 생태계의 핵심 도구들이 모두 Go로 작성되었습니다. 2025년 Stack Overflow 설문에서 Go는 "가장 배우고 싶은 언어" 3위, "가장 연봉이 높은 언어" 4위에 올랐으며, Indeed 기준 Go 개발자 구인은 전년 대비 15% 증가했습니다.
Go가 선택받는 이유는 명확합니다:
- 단순함: 키워드 25개, 클래스 없음, 상속 없음
- 동시성: 고루틴과 채널로 수백만 동시 작업 처리
- 빠른 컴파일: 수백만 줄 코드도 수 초 내 컴파일
- 단일 바이너리: 의존성 없는 정적 바이너리로 배포 극도로 단순
- 성능: C/C++에 근접한 실행 속도, Java/Python보다 메모리 효율적
Go 사용 기업 및 프로젝트:
┌─────────────────────────────────────────────────────────┐
│ Google - 내부 인프라, gRPC, Kubernetes 원저자 │
│ Uber - 고성능 지오펜싱 서비스 (Go로 재작성) │
│ Cloudflare - 엣지 프록시, DNS, Workers 런타임 │
│ Twitch - 실시간 채팅 시스템 (수백만 동시 접속) │
│ Docker - 컨테이너 런타임 전체가 Go │
│ Terraform - HashiCorp 전 제품군이 Go │
│ CockroachDB - 분산 SQL 데이터베이스 │
│ Dropbox - 성능 핵심 서비스를 Python에서 Go로 마이그레이션 │
└─────────────────────────────────────────────────────────┘
이 가이드에서는 Go의 기초 문법부터 동시성 패턴, 프로덕션 레벨 서버 구축, CLI 도구 개발, Docker 최적화까지 체계적으로 다룹니다.
1. Go 기초: 타입, 구조체, 메서드, 포인터
1.1 기본 타입과 제로 값
Go의 모든 변수는 선언 시 제로 값(zero value)으로 초기화됩니다:
package main
import "fmt"
func main() {
// 기본 타입과 제로 값
var i int // 0
var f float64 // 0.0
var b bool // false
var s string // "" (빈 문자열)
// 짧은 선언 (타입 추론)
name := "gopher" // string
age := 10 // int
pi := 3.14 // float64
active := true // bool
// 상수
const MaxRetries = 3
const (
StatusOK = 200
StatusError = 500
)
fmt.Println(i, f, b, s, name, age, pi, active)
}
1.2 슬라이스 vs 배열
배열은 고정 크기이고, 슬라이스는 동적 크기의 배열 뷰입니다:
// 배열: 크기가 타입의 일부
var arr [5]int // [0, 0, 0, 0, 0]
matrix := [2][3]int{{1,2,3}, {4,5,6}}
// 슬라이스: 동적, 실무에서 99% 사용
slice := []int{1, 2, 3}
slice = append(slice, 4, 5) // [1, 2, 3, 4, 5]
// make로 생성 (길이, 용량)
buf := make([]byte, 0, 1024) // 길이 0, 용량 1024
// 슬라이싱 (원본 공유 주의!)
original := []int{1, 2, 3, 4, 5}
sub := original[1:3] // [2, 3] - 원본의 뷰
sub[0] = 99 // original도 변경됨: [1, 99, 3, 4, 5]
// 안전한 복사
copied := make([]int, len(original))
copy(copied, original)
1.3 맵(Map)
// 맵 생성
m := map[string]int{
"apple": 5,
"banana": 3,
}
// 존재 여부 확인 (comma ok 패턴)
val, ok := m["cherry"]
if !ok {
fmt.Println("cherry not found")
}
// 순회 (순서 비보장)
for key, value := range m {
fmt.Printf("%s: %d\n", key, value)
}
// 삭제
delete(m, "apple")
1.4 구조체와 메서드
type User struct {
ID int
Name string
Email string
CreatedAt time.Time
}
// 값 수신자 메서드 (읽기 전용)
func (u User) FullName() string {
return u.Name
}
// 포인터 수신자 메서드 (수정 가능)
func (u *User) UpdateEmail(email string) {
u.Email = email
}
// 생성자 패턴 (Go에는 생성자가 없으므로 함수로 대체)
func NewUser(name, email string) *User {
return &User{
Name: name,
Email: email,
CreatedAt: time.Now(),
}
}
1.5 포인터
Go의 포인터는 C와 달리 포인터 연산이 불가능하여 안전합니다:
func main() {
x := 42
p := &x // 주소 획득
fmt.Println(*p) // 역참조: 42
*p = 100
fmt.Println(x) // 100
// nil 포인터 체크
var ptr *int
if ptr != nil {
fmt.Println(*ptr)
}
}
// 값 전달 vs 포인터 전달
func doubleValue(n int) { n *= 2 } // 원본 불변
func doublePointer(n *int) { *n *= 2 } // 원본 수정
2. 인터페이스와 컴포지션
2.1 암시적 인터페이스 (덕 타이핑)
Go의 인터페이스는 Java/C#과 달리 명시적 선언이 필요 없습니다. 메서드를 구현하면 자동으로 인터페이스를 만족합니다:
// 인터페이스 정의
type Writer interface {
Write(p []byte) (n int, err error)
}
type Reader interface {
Read(p []byte) (n int, err error)
}
// 인터페이스 조합
type ReadWriter interface {
Reader
Writer
}
// MyBuffer는 Writer를 "명시적으로 선언하지 않아도" 만족
type MyBuffer struct {
data []byte
}
func (b *MyBuffer) Write(p []byte) (int, error) {
b.data = append(b.data, p...)
return len(p), nil
}
// io.Writer를 받는 함수에 MyBuffer 전달 가능
func SaveToWriter(w Writer, content string) error {
_, err := w.Write([]byte(content))
return err
}
2.2 작은 인터페이스의 힘
Go 표준 라이브러리의 핵심 철학은 작은 인터페이스입니다:
Go 표준 라이브러리 핵심 인터페이스:
┌──────────────────────────────────────────────┐
│ io.Reader - Read(p []byte) (n, err) │
│ io.Writer - Write(p []byte) (n, err) │
│ io.Closer - Close() error │
│ fmt.Stringer - String() string │
│ error - Error() string │
│ sort.Interface - Len, Less, Swap │
│ http.Handler - ServeHTTP(w, r) │
│ json.Marshaler - MarshalJSON() ([]byte, err) │
└──────────────────────────────────────────────┘
// 실용 예: 어떤 Reader든 받아서 처리
func CountLines(r io.Reader) (int, error) {
scanner := bufio.NewScanner(r)
count := 0
for scanner.Scan() {
count++
}
return count, scanner.Err()
}
// 파일, HTTP 응답, 문자열 등 모두 가능
lines1, _ := CountLines(os.Stdin)
lines2, _ := CountLines(resp.Body)
lines3, _ := CountLines(strings.NewReader("hello\nworld"))
2.3 구조체 임베딩 (컴포지션)
Go는 상속 대신 임베딩으로 코드를 재사용합니다:
type Animal struct {
Name string
}
func (a Animal) Speak() string {
return a.Name + " makes a sound"
}
// Dog은 Animal을 임베딩 (상속이 아닌 컴포지션)
type Dog struct {
Animal // 임베딩
Breed string
}
func main() {
d := Dog{
Animal: Animal{Name: "Buddy"},
Breed: "Labrador",
}
fmt.Println(d.Speak()) // Animal의 메서드 승격
fmt.Println(d.Name) // Animal의 필드 직접 접근
}
3. 동시성: 고루틴과 채널
3.1 고루틴
고루틴은 Go 런타임이 관리하는 경량 스레드입니다. OS 스레드가 약 1MB 스택인 반면, 고루틴은 약 2KB에서 시작합니다:
func main() {
// 고루틴 시작: go 키워드만 붙이면 됨
go func() {
fmt.Println("Hello from goroutine!")
}()
// 1000개 고루틴 동시 실행
for i := 0; i < 1000; i++ {
go func(id int) {
fmt.Printf("Worker %d\n", id)
}(i) // i를 인자로 전달 (클로저 변수 캡처 주의)
}
time.Sleep(time.Second) // 메인 고루틴 대기
}
3.2 채널 (Channel)
채널은 고루틴 간 데이터를 안전하게 전달하는 파이프입니다:
func main() {
// 버퍼 없는 채널 (동기)
ch := make(chan string)
go func() {
ch <- "hello" // 전송 (수신자가 올 때까지 블록)
}()
msg := <-ch // 수신 (전송자가 올 때까지 블록)
fmt.Println(msg)
// 버퍼 있는 채널 (비동기)
buffered := make(chan int, 3)
buffered <- 1 // 블록 안 됨 (버퍼에 여유)
buffered <- 2
buffered <- 3
// buffered <- 4 // 여기서 블록됨 (버퍼 가득)
// 채널 방향 제한
// chan<- int (전송 전용)
// <-chan int (수신 전용)
}
// 생산자-소비자 패턴
func producer(ch chan<- int) {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch) // 더 이상 보낼 데이터 없음
}
func consumer(ch <-chan int) {
for val := range ch { // close될 때까지 수신
fmt.Println(val)
}
}
3.3 Select 문
select는 여러 채널 연산을 동시에 대기합니다:
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "from ch1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "from ch2"
}()
// 먼저 준비된 채널 수신
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
case <-time.After(3 * time.Second):
fmt.Println("timeout!")
}
}
3.4 sync 패키지
// WaitGroup: 여러 고루틴 완료 대기
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Worker %d done\n", id)
}(i)
}
wg.Wait() // 모든 고루틴 완료까지 대기
}
// Mutex: 공유 자원 보호
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
// Once: 한 번만 실행
var once sync.Once
var instance *Database
func GetDB() *Database {
once.Do(func() {
instance = connectDB()
})
return instance
}
3.5 context.Context
context는 고루틴의 수명을 관리하고, 취소 신호와 타임아웃을 전파합니다:
func fetchURL(ctx context.Context, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return "", err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
return string(body), err
}
func main() {
// 3초 타임아웃
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
result, err := fetchURL(ctx, "https://api.example.com/data")
if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("Request timed out!")
return
}
fmt.Println(result)
}
4. 동시성 패턴
4.1 Fan-Out / Fan-In
여러 고루틴이 작업을 분산 처리(Fan-Out)하고 결과를 하나로 모으는(Fan-In) 패턴:
func fanOut(input <-chan int, workers int) []<-chan int {
channels := make([]<-chan int, workers)
for i := 0; i < workers; i++ {
channels[i] = process(input)
}
return channels
}
func process(input <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range input {
out <- n * n // 제곱 연산
}
}()
return out
}
func fanIn(channels ...<-chan int) <-chan int {
var wg sync.WaitGroup
merged := make(chan int)
for _, ch := range channels {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for val := range c {
merged <- val
}
}(ch)
}
go func() {
wg.Wait()
close(merged)
}()
return merged
}
4.2 워커 풀 (Worker Pool)
func workerPool(jobs <-chan Job, results chan<- Result, numWorkers int) {
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for job := range jobs {
result := processJob(job)
results <- result
}
}(i)
}
go func() {
wg.Wait()
close(results)
}()
}
4.3 파이프라인
func generator(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- n * n
}
}()
return out
}
func filter(in <-chan int, predicate func(int) bool) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
if predicate(n) {
out <- n
}
}
}()
return out
}
// 사용: 파이프라인 조합
func main() {
nums := generator(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
squares := square(nums)
evens := filter(squares, func(n int) bool { return n%2 == 0 })
for result := range evens {
fmt.Println(result) // 4, 16, 36, 64, 100
}
}
4.4 Rate Limiter와 세마포어
// Rate Limiter
func rateLimitedFetch(urls []string, rps int) {
ticker := time.NewTicker(time.Second / time.Duration(rps))
defer ticker.Stop()
for _, url := range urls {
<-ticker.C // 초당 rps번만 실행
go fetch(url)
}
}
// 세마포어 패턴 (동시 실행 수 제한)
func semaphorePattern(tasks []Task, maxConcurrent int) {
sem := make(chan struct{}, maxConcurrent)
var wg sync.WaitGroup
for _, task := range tasks {
wg.Add(1)
sem <- struct{}{} // 슬롯 획득
go func(t Task) {
defer wg.Done()
defer func() { <-sem }() // 슬롯 반환
process(t)
}(task)
}
wg.Wait()
}
4.5 errgroup 패턴
import "golang.org/x/sync/errgroup"
func fetchAll(ctx context.Context, urls []string) ([]string, error) {
g, ctx := errgroup.WithContext(ctx)
results := make([]string, len(urls))
for i, url := range urls {
i, url := i, url // 루프 변수 캡처
g.Go(func() error {
body, err := fetchURL(ctx, url)
if err != nil {
return fmt.Errorf("fetching %s: %w", url, err)
}
results[i] = body
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err // 첫 번째 에러 반환, 나머지 취소
}
return results, nil
}
5. 에러 처리
5.1 기본 에러 패턴
Go는 예외(exception) 대신 값으로 에러를 반환합니다:
// 기본 에러 반환
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// fmt.Errorf로 컨텍스트 추가
func readConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading config %s: %w", path, err)
}
// ...
return config, nil
}
5.2 Sentinel 에러와 커스텀 에러
// Sentinel 에러 (패키지 레벨 변수)
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrConflict = errors.New("conflict")
)
// 커스텀 에러 타입
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error: %s - %s", e.Field, e.Message)
}
// errors.Is / errors.As 사용
func handleError(err error) {
// Sentinel 에러 비교
if errors.Is(err, ErrNotFound) {
// 404 처리
}
// 커스텀 에러 타입 추출
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("Field: %s, Message: %s\n", valErr.Field, valErr.Message)
}
}
5.3 Defer, Panic, Recover
// defer: 함수 종료 시 역순 실행 (리소스 정리에 핵심)
func readFile(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close() // 함수 종료 시 반드시 실행
data, err := io.ReadAll(f)
return string(data), err
}
// panic/recover: 정말 복구 불가능한 상황에서만 사용
func safeExecute(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
}
}()
fn()
return nil
}
6. 제네릭 (Go 1.18+)
6.1 기본 문법
// 제네릭 함수
func Map[T any, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}
func Filter[T any](slice []T, predicate func(T) bool) []T {
var result []T
for _, v := range slice {
if predicate(v) {
result = append(result, v)
}
}
return result
}
// 사용
nums := []int{1, 2, 3, 4, 5}
doubled := Map(nums, func(n int) int { return n * 2 })
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
6.2 타입 제약 (Constraints)
// 커스텀 제약
type Number interface {
~int | ~int32 | ~int64 | ~float32 | ~float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
// comparable 제약 (맵 키로 사용 가능한 타입)
func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}
// 제네릭 구조체
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
}
6.3 제네릭 사용 가이드라인
제네릭 사용 판단 기준:
┌─────────────────────────────────────────────────────────┐
│ 사용하면 좋은 경우 │ 피해야 하는 경우 │
│ ─────────────────────────────── │ ────────────────────── │
│ 컬렉션 유틸리티 (Map, Filter) │ 단 2-3개 타입만 지원 │
│ 자료구조 (Stack, Queue, Tree) │ 인터페이스로 충분할 때 │
│ 타입 안전한 결과/옵션 타입 │ 코드 가독성이 떨어질 때 │
│ 정렬, 검색 알고리즘 │ 리플렉션이 필요한 경우 │
└─────────────────────────────────────────────────────────┘
7. 테스트
7.1 테이블 주도 테스트 (Table-Driven Tests)
Go의 관용적 테스트 패턴:
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -1, -2, -3},
{"zero", 0, 0, 0},
{"mixed", -1, 5, 4},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d, want %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}
7.2 Testify 라이브러리
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUser(t *testing.T) {
user, err := CreateUser("John", "john@example.com")
require.NoError(t, err) // 에러 시 즉시 중단
assert.Equal(t, "John", user.Name) // 에러 시 계속 진행
assert.NotEmpty(t, user.ID)
assert.WithinDuration(t, time.Now(), user.CreatedAt, time.Second)
}
7.3 HTTP 테스트
func TestGetUserHandler(t *testing.T) {
// 테스트 서버 설정
handler := http.HandlerFunc(GetUserHandler)
req := httptest.NewRequest("GET", "/users/123", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
var user User
err := json.NewDecoder(rec.Body).Decode(&user)
require.NoError(t, err)
assert.Equal(t, "123", user.ID)
}
7.4 벤치마크와 퍼징
// 벤치마크
func BenchmarkFibonacci(b *testing.B) {
for i := 0; i < b.N; i++ {
Fibonacci(20)
}
}
// 퍼징 (Go 1.18+)
func FuzzParseJSON(f *testing.F) {
// 시드 코퍼스
f.Add([]byte(`{"name": "test"}`))
f.Add([]byte(`{}`))
f.Add([]byte(`[]`))
f.Fuzz(func(t *testing.T, data []byte) {
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
return // 유효하지 않은 JSON은 무시
}
// 다시 마샬링하면 에러 없어야 함
_, err := json.Marshal(result)
if err != nil {
t.Errorf("re-marshal failed: %v", err)
}
})
}
# 테스트 실행
go test ./... # 전체 테스트
go test -v -run TestUser ./pkg/user # 특정 테스트
go test -bench=. ./... # 벤치마크
go test -fuzz=FuzzParseJSON ./... # 퍼징
go test -race ./... # 레이스 디텍터
go test -cover ./... # 커버리지
8. REST API 구축
8.1 Chi 라우터 + 미들웨어
package main
import (
"encoding/json"
"log"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
type Server struct {
router *chi.Mux
db *sql.DB
}
func NewServer(db *sql.DB) *Server {
s := &Server{
router: chi.NewRouter(),
db: db,
}
s.routes()
return s
}
func (s *Server) routes() {
// 미들웨어
s.router.Use(middleware.Logger)
s.router.Use(middleware.Recoverer)
s.router.Use(middleware.Timeout(30 * time.Second))
s.router.Use(corsMiddleware)
// 라우트
s.router.Route("/api/v1", func(r chi.Router) {
r.Get("/health", s.handleHealth)
r.Route("/users", func(r chi.Router) {
r.Get("/", s.handleListUsers)
r.Post("/", s.handleCreateUser)
r.Route("/{userID}", func(r chi.Router) {
r.Get("/", s.handleGetUser)
r.Put("/", s.handleUpdateUser)
r.Delete("/", s.handleDeleteUser)
})
})
})
}
func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) {
var input CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
respondError(w, http.StatusBadRequest, "invalid request body")
return
}
user, err := s.userService.Create(r.Context(), input)
if err != nil {
respondError(w, http.StatusInternalServerError, err.Error())
return
}
respondJSON(w, http.StatusCreated, user)
}
// 헬퍼 함수
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func respondError(w http.ResponseWriter, status int, message string) {
respondJSON(w, status, map[string]string{"error": message})
}
8.2 데이터베이스 (sqlx)
import "github.com/jmoiron/sqlx"
type UserRepository struct {
db *sqlx.DB
}
func (r *UserRepository) GetByID(ctx context.Context, id string) (*User, error) {
var user User
err := r.db.GetContext(ctx, &user,
"SELECT id, name, email, created_at FROM users WHERE id = $1", id)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return &user, err
}
func (r *UserRepository) List(ctx context.Context, limit, offset int) ([]User, error) {
var users []User
err := r.db.SelectContext(ctx, &users,
"SELECT id, name, email, created_at FROM users ORDER BY created_at DESC LIMIT $1 OFFSET $2",
limit, offset)
return users, err
}
9. gRPC 서비스 구축
9.1 Protocol Buffers 정의
// proto/user/v1/user.proto
syntax = "proto3";
package user.v1;
option go_package = "gen/user/v1;userv1";
service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse);
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
rpc StreamUpdates(StreamUpdatesRequest) returns (stream UserEvent);
}
message User {
string id = 1;
string name = 2;
string email = 3;
int64 created_at = 4;
}
message GetUserRequest {
string id = 1;
}
message GetUserResponse {
User user = 1;
}
9.2 gRPC 서버 구현
type userServer struct {
userv1.UnimplementedUserServiceServer
repo UserRepository
}
func (s *userServer) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) {
user, err := s.repo.GetByID(ctx, req.Id)
if err != nil {
if errors.Is(err, ErrNotFound) {
return nil, status.Error(codes.NotFound, "user not found")
}
return nil, status.Error(codes.Internal, "internal error")
}
return &userv1.GetUserResponse{User: toProtoUser(user)}, nil
}
// 서버 스트리밍
func (s *userServer) StreamUpdates(req *userv1.StreamUpdatesRequest, stream userv1.UserService_StreamUpdatesServer) error {
for event := range s.eventCh {
if err := stream.Send(event); err != nil {
return err
}
}
return nil
}
// 인터셉터 (미들웨어)
func loggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
start := time.Now()
resp, err := handler(ctx, req)
log.Printf("method=%s duration=%s error=%v", info.FullMethod, time.Since(start), err)
return resp, err
}
10. CLI 도구 개발
10.1 Cobra + Viper
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var rootCmd = &cobra.Command{
Use: "mytool",
Short: "A powerful CLI tool",
Long: "mytool is a CLI application for managing deployments",
}
var deployCmd = &cobra.Command{
Use: "deploy [environment]",
Short: "Deploy application",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
env := args[0]
image, _ := cmd.Flags().GetString("image")
replicas, _ := cmd.Flags().GetInt("replicas")
dryRun, _ := cmd.Flags().GetBool("dry-run")
fmt.Printf("Deploying to %s: image=%s, replicas=%d, dry-run=%v\n",
env, image, replicas, dryRun)
if dryRun {
fmt.Println("Dry run mode - no changes applied")
return nil
}
return executeDeploy(env, image, replicas)
},
}
func init() {
// 플래그 설정
deployCmd.Flags().StringP("image", "i", "", "Container image (required)")
deployCmd.Flags().IntP("replicas", "r", 1, "Number of replicas")
deployCmd.Flags().Bool("dry-run", false, "Dry run mode")
deployCmd.MarkFlagRequired("image")
// Viper로 설정 파일 바인딩
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
viper.AutomaticEnv()
rootCmd.AddCommand(deployCmd)
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
10.2 BubbleTea TUI 예제
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type model struct {
choices []string
cursor int
selected map[int]struct{}
}
func (m model) Init() tea.Cmd { return nil }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "q", "ctrl+c":
return m, tea.Quit
case "up", "k":
if m.cursor > 0 {
m.cursor--
}
case "down", "j":
if m.cursor < len(m.choices)-1 {
m.cursor++
}
case "enter", " ":
if _, ok := m.selected[m.cursor]; ok {
delete(m.selected, m.cursor)
} else {
m.selected[m.cursor] = struct{}{}
}
}
}
return m, nil
}
func (m model) View() string {
s := "Select items:\n\n"
for i, choice := range m.choices {
cursor := " "
if m.cursor == i {
cursor = ">"
}
checked := " "
if _, ok := m.selected[i]; ok {
checked = "x"
}
s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
}
s += "\nPress q to quit.\n"
return s
}
11. 프로덕션 배포
11.1 Docker 멀티스테이지 빌드 (5MB 바이너리)
# 빌드 스테이지
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-s -w" -o /app/server ./cmd/server
# 실행 스테이지 (scratch = 빈 이미지)
FROM scratch
# TLS 인증서 (HTTPS 호출 필요 시)
COPY /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# 바이너리만 복사
COPY /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
# 빌드 결과 크기 비교
# Go scratch: ~5-10MB
# Go alpine: ~15MB
# Node.js: ~200MB+
# Java Spring: ~300MB+
# Python: ~150MB+
11.2 Graceful Shutdown
func main() {
srv := &http.Server{
Addr: ":8080",
Handler: router,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
// 서버 시작 (별도 고루틴)
go func() {
log.Printf("Server starting on :8080")
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("Server error: %v", err)
}
}()
// 시그널 대기 (SIGINT, SIGTERM)
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down gracefully...")
// 30초 타임아웃으로 기존 요청 완료 대기
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("Forced shutdown: %v", err)
}
log.Println("Server stopped")
}
11.3 pprof 프로파일링
import _ "net/http/pprof"
func main() {
// pprof 엔드포인트 노출 (별도 포트 권장)
go func() {
log.Println(http.ListenAndServe(":6060", nil))
}()
// 메인 서버 실행
// ...
}
# CPU 프로파일
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# 메모리 프로파일
go tool pprof http://localhost:6060/debug/pprof/heap
# 고루틴 프로파일
go tool pprof http://localhost:6060/debug/pprof/goroutine
# 웹 UI
go tool pprof -http=:8081 profile.out
11.4 레이스 디텍터
# 테스트 시 레이스 감지
go test -race ./...
# 빌드 시 레이스 감지 포함
go build -race -o server ./cmd/server
12. Go vs Rust vs Java 비교
┌─────────────────┬──────────────────┬──────────────────┬──────────────────┐
│ 항목 │ Go │ Rust │ Java │
├─────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ 학습 곡선 │ 낮음 (1-2주) │ 높음 (3-6개월) │ 중간 (1-2개월) │
│ 컴파일 속도 │ 매우 빠름 │ 느림 │ 보통 │
│ 실행 속도 │ 빠름 │ 매우 빠름 │ 빠름 (JIT) │
│ 메모리 관리 │ GC │ 소유권 시스템 │ GC │
│ 동시성 │ 고루틴/채널 │ async/tokio │ Virtual Thread │
│ 바이너리 크기 │ 5-15MB │ 1-5MB │ 100MB+ (JRE) │
│ 생태계 │ 클라우드/인프라 │ 시스템/WebAssembly│ 엔터프라이즈 │
│ 에러 처리 │ 값 반환 │ Result/Option │ 예외(Exception) │
│ 제네릭 │ 1.18+ (기본) │ 강력함 │ 완전 지원 │
│ 주요 사용처 │ 마이크로서비스, │ OS, 게임 엔진, │ 대규모 엔터프 │
│ │ CLI, DevOps │ 임베디드 │ 라이즈, 안드로이드│
│ 기업 채택 │ Google, Uber │ Mozilla, AWS │ 대부분의 대기업 │
│ 연봉 (US) │ 높음 (상위 5) │ 매우 높음 (1위) │ 보통 │
└─────────────────┴──────────────────┴──────────────────┴──────────────────┘
Go가 적합한 경우: 마이크로서비스, API 서버, CLI 도구, DevOps 도구, 네트워크 프로그래밍 Rust가 적합한 경우: 시스템 프로그래밍, 게임 엔진, 임베디드, 최대 성능이 필요한 경우 Java가 적합한 경우: 대규모 엔터프라이즈, 안드로이드 앱, 기존 레거시 시스템
13. 면접 질문 15선
기초
-
Go에서 슬라이스와 배열의 차이는? 배열은 고정 크기로 타입의 일부이고(예:
[5]int), 슬라이스는 동적 크기의 배열 뷰입니다. 슬라이스는 내부적으로 포인터, 길이, 용량을 가집니다. -
Go에 상속이 없는 이유는? 어떻게 코드를 재사용하나요? Go는 상속 대신 구조체 임베딩(컴포지션)을 사용합니다. "상속보다 컴포지션" 원칙을 언어 레벨에서 강제합니다.
-
인터페이스가 nil인 경우와 nil 포인터를 가진 인터페이스의 차이는? 인터페이스는 (type, value) 쌍입니다. 둘 다 nil이면 nil 인터페이스이고, type은 있지만 value가 nil이면 non-nil 인터페이스입니다.
-
defer의 실행 순서와 주의사항은? LIFO(후입선출) 순서로 실행됩니다. defer 시점의 인자 값이 캡처되므로, 루프에서 defer 사용 시 주의가 필요합니다.
-
Go의 제로 값(zero value) 철학은? 모든 변수가 선언 시 유효한 기본값을 가지므로 초기화되지 않은 변수 버그를 방지합니다. 이로 인해 생성자가 덜 필요합니다.
동시성
-
고루틴이 OS 스레드보다 가벼운 이유는? 고루틴은 약 2KB 스택으로 시작하고(OS 스레드는 1MB+), Go 런타임의 M:N 스케줄러가 수천 개 고루틴을 소수 OS 스레드에 매핑합니다.
-
버퍼 채널과 언버퍼 채널의 차이는? 언버퍼 채널은 송신과 수신이 동기적으로 만나야 합니다. 버퍼 채널은 용량만큼 비동기 전송 가능합니다.
-
고루틴 누수(leak)가 발생하는 상황과 방지법은? 채널에 전송/수신할 상대가 없거나, context 취소를 처리하지 않으면 발생합니다. context.WithCancel/Timeout 사용이 핵심입니다.
-
데드락이 발생하는 조건과 Go에서의 감지 방법은? Go 런타임이 모든 고루틴이 블록되면 감지하여 panic합니다. go vet과 race detector도 도움됩니다.
-
sync.Mutex vs sync.RWMutex 사용 기준은? 읽기가 쓰기보다 훨씬 많으면 RWMutex가 유리합니다. 여러 고루틴이 동시에 읽기 가능하기 때문입니다.
고급
-
context.Context의 주요 용도 3가지는? 취소 전파(Cancel), 타임아웃(Timeout/Deadline), 값 전달(WithValue). HTTP 핸들러와 DB 호출에 항상 사용해야 합니다.
-
Go의 GC(Garbage Collector) 특성은? Go는 동시 마크앤스윕(concurrent mark-and-sweep) GC를 사용하며, STW(stop-the-world) 시간이 1ms 미만입니다. GOGC 환경변수로 조절 가능합니다.
-
제네릭 도입 전후의 코드 차이를 설명하세요. 도입 전에는 interface와 타입 단언(type assertion) 또는 코드 생성이 필요했습니다. 도입 후에는 타입 파라미터로 컴파일 타임 타입 안전성을 제공합니다.
-
Go에서 의존성 주입(DI)은 어떻게 하나요? 프레임워크 없이 생성자 함수를 통해 인터페이스를 주입합니다. wire(Google), fx(Uber) 같은 DI 프레임워크도 있습니다.
-
Go 모듈 시스템에서 indirect 의존성 관리는? go.mod의 indirect 주석은 직접 임포트하지 않는 의존성입니다. go mod tidy로 정리하고, go mod vendor로 벤더링할 수 있습니다.
14. 실전 퀴즈
Q1: 다음 코드의 출력은?
func main() {
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i)
}()
}
time.Sleep(time.Second)
}
답: Go 1.22 이전에는 대부분 3, 3, 3이 출력됩니다. 클로저가 루프 변수 i를 참조하는데, 고루틴이 실행될 때 이미 루프가 종료되어 i=3입니다. Go 1.22부터는 루프 변수가 각 반복에서 새로 생성되어 0, 1, 2(순서 불확정)가 출력됩니다.
Q2: 이 코드에서 데드락이 발생하는 이유는?
func main() {
ch := make(chan int)
ch <- 42
fmt.Println(<-ch)
}
답: 언버퍼 채널에 전송하면 수신자가 나타날 때까지 블록됩니다. 메인 고루틴이 ch <- 42에서 블록되어 <-ch 수신에 도달할 수 없으므로 데드락입니다. 해결: 별도 고루틴에서 전송하거나 버퍼 채널 사용.
Q3: nil 인터페이스 문제 - 왜 err != nil이 true인가?
type MyError struct{}
func (e *MyError) Error() string { return "error" }
func doSomething() error {
var err *MyError = nil
return err
}
func main() {
err := doSomething()
fmt.Println(err == nil) // false!
}
답: doSomething()은 nil 포인터를 가진 *MyError 타입의 인터페이스를 반환합니다. 인터페이스는 (type=MyError, value=nil)이므로 nil이 아닙니다. 해결: 함수에서 명시적으로 return nil을 반환합니다.
Q4: WaitGroup 사용 시 흔한 실수는?
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
go func(id int) {
wg.Add(1) // 잘못된 위치!
defer wg.Done()
fmt.Println(id)
}(i)
}
wg.Wait()
}
답: wg.Add(1)이 고루틴 안에 있으므로 wg.Wait()가 먼저 실행될 수 있습니다. 올바른 방법은 고루틴 시작 전에 wg.Add(1)을 호출하는 것입니다.
Q5: context 취소가 전파되는 원리를 설명하세요.
답: context.WithCancel은 부모 컨텍스트의 Done 채널을 감시하는 자식 컨텍스트를 생성합니다. 부모가 취소되면 자식의 Done 채널도 닫힙니다. 이 체인은 최상위까지 전파되어, 하나의 취소로 전체 요청 트리를 정리할 수 있습니다. select 문에서 ctx.Done()을 확인하여 고루틴이 적절히 종료해야 합니다.
참고 자료
- Go 공식 문서
- Effective Go
- Go by Example
- Go Concurrency Patterns (Rob Pike)
- Advanced Go Concurrency Patterns
- The Go Programming Language (Donovan & Kernighan)
- Concurrency in Go (Katherine Cox-Buday)
- Go Wiki: Table Driven Tests
- Go Wiki: Code Review Comments
- Uber Go Style Guide
- Go Module Reference
- Go Generics Tutorial
- pprof 가이드
- Chi Router
- gRPC Go 공식 가이드
- Cobra CLI 라이브러리
- BubbleTea TUI 프레임워크
Go Language Complete Guide 2025: From Goroutines, Channels, Interfaces to Production Patterns
- Introduction: Why Go in 2025?
- 1. Go Fundamentals: Types, Structs, Methods, Pointers
- 2. Interfaces and Composition
- 3. Concurrency: Goroutines and Channels
- 4. Concurrency Patterns
- 5. Error Handling
- 6. Generics (Go 1.18+)
- 7. Testing
- 8. Building REST APIs
- 9. Building gRPC Services
- 10. CLI Tool Development
- 11. Production Deployment
- 12. Go vs Rust vs Java Comparison
- 13. Interview Questions (15)
- 14. Practice Quiz
- References
Introduction: Why Go in 2025?
Kubernetes, Docker, Terraform, Prometheus, Istio — the core tools of the cloud-native ecosystem are all written in Go. In the 2025 Stack Overflow Survey, Go ranked 3rd in "most wanted languages" and 4th in "highest-paying languages," while Go developer job postings increased 15% year-over-year on Indeed.
The reasons Go gets chosen are clear:
- Simplicity: 25 keywords, no classes, no inheritance
- Concurrency: Millions of concurrent tasks with goroutines and channels
- Fast compilation: Millions of lines compile in seconds
- Single binary: Static binaries with zero dependencies make deployment trivially simple
- Performance: Near C/C++ execution speed, more memory-efficient than Java/Python
Companies and Projects Using Go:
┌───────────────────────────────────────────────────────────┐
│ Google - Internal infra, gRPC, original K8s authors │
│ Uber - High-perf geofencing service (rewrite) │
│ Cloudflare - Edge proxies, DNS, Workers runtime │
│ Twitch - Real-time chat (millions of connections) │
│ Docker - Entire container runtime in Go │
│ Terraform - All HashiCorp products in Go │
│ CockroachDB - Distributed SQL database │
│ Dropbox - Migrated perf-critical services from Python│
└───────────────────────────────────────────────────────────┘
This guide systematically covers Go from basic syntax to concurrency patterns, production-level server construction, CLI tool development, and Docker optimization.
1. Go Fundamentals: Types, Structs, Methods, Pointers
1.1 Basic Types and Zero Values
Every variable in Go is initialized to its zero value upon declaration:
package main
import "fmt"
func main() {
// Basic types and zero values
var i int // 0
var f float64 // 0.0
var b bool // false
var s string // "" (empty string)
// Short declaration (type inference)
name := "gopher" // string
age := 10 // int
pi := 3.14 // float64
active := true // bool
// Constants
const MaxRetries = 3
const (
StatusOK = 200
StatusError = 500
)
fmt.Println(i, f, b, s, name, age, pi, active)
}
1.2 Slices vs Arrays
Arrays have fixed size; slices are dynamic-size views over arrays:
// Array: size is part of the type
var arr [5]int // [0, 0, 0, 0, 0]
matrix := [2][3]int{{1,2,3}, {4,5,6}}
// Slice: dynamic, used 99% in practice
slice := []int{1, 2, 3}
slice = append(slice, 4, 5) // [1, 2, 3, 4, 5]
// Create with make (length, capacity)
buf := make([]byte, 0, 1024) // length 0, capacity 1024
// Slicing (shares underlying array!)
original := []int{1, 2, 3, 4, 5}
sub := original[1:3] // [2, 3] - view of original
sub[0] = 99 // original also changes: [1, 99, 3, 4, 5]
// Safe copy
copied := make([]int, len(original))
copy(copied, original)
1.3 Maps
// Create map
m := map[string]int{
"apple": 5,
"banana": 3,
}
// Check existence (comma ok pattern)
val, ok := m["cherry"]
if !ok {
fmt.Println("cherry not found")
}
// Iteration (order not guaranteed)
for key, value := range m {
fmt.Printf("%s: %d\n", key, value)
}
// Delete
delete(m, "apple")
1.4 Structs and Methods
type User struct {
ID int
Name string
Email string
CreatedAt time.Time
}
// Value receiver method (read-only)
func (u User) FullName() string {
return u.Name
}
// Pointer receiver method (can modify)
func (u *User) UpdateEmail(email string) {
u.Email = email
}
// Constructor pattern (Go has no constructors, use functions)
func NewUser(name, email string) *User {
return &User{
Name: name,
Email: email,
CreatedAt: time.Now(),
}
}
1.5 Pointers
Go pointers are safe — unlike C, pointer arithmetic is not allowed:
func main() {
x := 42
p := &x // get address
fmt.Println(*p) // dereference: 42
*p = 100
fmt.Println(x) // 100
// nil pointer check
var ptr *int
if ptr != nil {
fmt.Println(*ptr)
}
}
// Value passing vs pointer passing
func doubleValue(n int) { n *= 2 } // original unchanged
func doublePointer(n *int) { *n *= 2 } // original modified
2. Interfaces and Composition
2.1 Implicit Interfaces (Duck Typing)
Go interfaces unlike Java/C# require no explicit declaration. Implementing the methods automatically satisfies the interface:
// Interface definition
type Writer interface {
Write(p []byte) (n int, err error)
}
type Reader interface {
Read(p []byte) (n int, err error)
}
// Interface composition
type ReadWriter interface {
Reader
Writer
}
// MyBuffer satisfies Writer without explicit declaration
type MyBuffer struct {
data []byte
}
func (b *MyBuffer) Write(p []byte) (int, error) {
b.data = append(b.data, p...)
return len(p), nil
}
// Can pass MyBuffer to any function accepting Writer
func SaveToWriter(w Writer, content string) error {
_, err := w.Write([]byte(content))
return err
}
2.2 The Power of Small Interfaces
The Go standard library's core philosophy is small interfaces:
Go Standard Library Core Interfaces:
┌──────────────────────────────────────────────────┐
│ io.Reader - Read(p []byte) (n, err) │
│ io.Writer - Write(p []byte) (n, err) │
│ io.Closer - Close() error │
│ fmt.Stringer - String() string │
│ error - Error() string │
│ sort.Interface - Len, Less, Swap │
│ http.Handler - ServeHTTP(w, r) │
│ json.Marshaler - MarshalJSON() ([]byte, err) │
└──────────────────────────────────────────────────┘
// Practical example: process any Reader
func CountLines(r io.Reader) (int, error) {
scanner := bufio.NewScanner(r)
count := 0
for scanner.Scan() {
count++
}
return count, scanner.Err()
}
// Works with files, HTTP responses, strings, etc.
lines1, _ := CountLines(os.Stdin)
lines2, _ := CountLines(resp.Body)
lines3, _ := CountLines(strings.NewReader("hello\nworld"))
2.3 Struct Embedding (Composition)
Go uses embedding instead of inheritance for code reuse:
type Animal struct {
Name string
}
func (a Animal) Speak() string {
return a.Name + " makes a sound"
}
// Dog embeds Animal (composition, not inheritance)
type Dog struct {
Animal // embedding
Breed string
}
func main() {
d := Dog{
Animal: Animal{Name: "Buddy"},
Breed: "Labrador",
}
fmt.Println(d.Speak()) // Animal's method promoted
fmt.Println(d.Name) // Direct access to Animal's field
}
3. Concurrency: Goroutines and Channels
3.1 Goroutines
Goroutines are lightweight threads managed by the Go runtime. While OS threads use about 1MB stack, goroutines start at about 2KB:
func main() {
// Start goroutine: just add the go keyword
go func() {
fmt.Println("Hello from goroutine!")
}()
// 1000 goroutines running concurrently
for i := 0; i < 1000; i++ {
go func(id int) {
fmt.Printf("Worker %d\n", id)
}(i) // pass i as argument (beware closure variable capture)
}
time.Sleep(time.Second) // wait for main goroutine
}
3.2 Channels
Channels are pipes for safely passing data between goroutines:
func main() {
// Unbuffered channel (synchronous)
ch := make(chan string)
go func() {
ch <- "hello" // send (blocks until receiver arrives)
}()
msg := <-ch // receive (blocks until sender arrives)
fmt.Println(msg)
// Buffered channel (asynchronous)
buffered := make(chan int, 3)
buffered <- 1 // no block (buffer has space)
buffered <- 2
buffered <- 3
// buffered <- 4 // blocks here (buffer full)
}
// Producer-consumer pattern
func producer(ch chan<- int) {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch) // no more data to send
}
func consumer(ch <-chan int) {
for val := range ch { // receive until closed
fmt.Println(val)
}
}
3.3 Select Statement
Select waits on multiple channel operations simultaneously:
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "from ch1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "from ch2"
}()
// Receive from whichever is ready first
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
case <-time.After(3 * time.Second):
fmt.Println("timeout!")
}
}
3.4 sync Package
// WaitGroup: wait for multiple goroutines to finish
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Worker %d done\n", id)
}(i)
}
wg.Wait() // wait until all goroutines complete
}
// Mutex: protect shared resources
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
// Once: execute only once
var once sync.Once
var instance *Database
func GetDB() *Database {
once.Do(func() {
instance = connectDB()
})
return instance
}
3.5 context.Context
Context manages goroutine lifetimes, propagating cancellation signals and timeouts:
func fetchURL(ctx context.Context, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return "", err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
return string(body), err
}
func main() {
// 3-second timeout
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
result, err := fetchURL(ctx, "https://api.example.com/data")
if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("Request timed out!")
return
}
fmt.Println(result)
}
4. Concurrency Patterns
4.1 Fan-Out / Fan-In
Multiple goroutines distribute work (Fan-Out) and merge results into one (Fan-In):
func fanOut(input <-chan int, workers int) []<-chan int {
channels := make([]<-chan int, workers)
for i := 0; i < workers; i++ {
channels[i] = process(input)
}
return channels
}
func process(input <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range input {
out <- n * n // square operation
}
}()
return out
}
func fanIn(channels ...<-chan int) <-chan int {
var wg sync.WaitGroup
merged := make(chan int)
for _, ch := range channels {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for val := range c {
merged <- val
}
}(ch)
}
go func() {
wg.Wait()
close(merged)
}()
return merged
}
4.2 Worker Pool
func workerPool(jobs <-chan Job, results chan<- Result, numWorkers int) {
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for job := range jobs {
result := processJob(job)
results <- result
}
}(i)
}
go func() {
wg.Wait()
close(results)
}()
}
4.3 Pipeline
func generator(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- n * n
}
}()
return out
}
func filter(in <-chan int, predicate func(int) bool) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
if predicate(n) {
out <- n
}
}
}()
return out
}
// Usage: compose pipeline
func main() {
nums := generator(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
squares := square(nums)
evens := filter(squares, func(n int) bool { return n%2 == 0 })
for result := range evens {
fmt.Println(result) // 4, 16, 36, 64, 100
}
}
4.4 Rate Limiter and Semaphore
// Rate Limiter
func rateLimitedFetch(urls []string, rps int) {
ticker := time.NewTicker(time.Second / time.Duration(rps))
defer ticker.Stop()
for _, url := range urls {
<-ticker.C // execute only rps times per second
go fetch(url)
}
}
// Semaphore pattern (limit concurrent execution)
func semaphorePattern(tasks []Task, maxConcurrent int) {
sem := make(chan struct{}, maxConcurrent)
var wg sync.WaitGroup
for _, task := range tasks {
wg.Add(1)
sem <- struct{}{} // acquire slot
go func(t Task) {
defer wg.Done()
defer func() { <-sem }() // release slot
process(t)
}(task)
}
wg.Wait()
}
4.5 errgroup Pattern
import "golang.org/x/sync/errgroup"
func fetchAll(ctx context.Context, urls []string) ([]string, error) {
g, ctx := errgroup.WithContext(ctx)
results := make([]string, len(urls))
for i, url := range urls {
i, url := i, url // capture loop variables
g.Go(func() error {
body, err := fetchURL(ctx, url)
if err != nil {
return fmt.Errorf("fetching %s: %w", url, err)
}
results[i] = body
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err // returns first error, cancels rest
}
return results, nil
}
5. Error Handling
5.1 Basic Error Patterns
Go returns errors as values instead of using exceptions:
// Basic error return
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Add context with fmt.Errorf
func readConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading config %s: %w", path, err)
}
// ...
return config, nil
}
5.2 Sentinel Errors and Custom Errors
// Sentinel errors (package-level variables)
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrConflict = errors.New("conflict")
)
// Custom error type
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error: %s - %s", e.Field, e.Message)
}
// Using errors.Is / errors.As
func handleError(err error) {
// Sentinel error comparison
if errors.Is(err, ErrNotFound) {
// handle 404
}
// Extract custom error type
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("Field: %s, Message: %s\n", valErr.Field, valErr.Message)
}
}
5.3 Defer, Panic, Recover
// defer: execute in reverse order at function exit (key for resource cleanup)
func readFile(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close() // always executes when function returns
data, err := io.ReadAll(f)
return string(data), err
}
// panic/recover: use only for truly unrecoverable situations
func safeExecute(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
}
}()
fn()
return nil
}
6. Generics (Go 1.18+)
6.1 Basic Syntax
// Generic function
func Map[T any, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}
func Filter[T any](slice []T, predicate func(T) bool) []T {
var result []T
for _, v := range slice {
if predicate(v) {
result = append(result, v)
}
}
return result
}
// Usage
nums := []int{1, 2, 3, 4, 5}
doubled := Map(nums, func(n int) int { return n * 2 })
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
6.2 Type Constraints
// Custom constraint
type Number interface {
~int | ~int32 | ~int64 | ~float32 | ~float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
// comparable constraint (types usable as map keys)
func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}
// Generic struct
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
}
6.3 When to Use Generics
Generics Decision Guide:
┌─────────────────────────────────────────────────────────┐
│ Good Use Cases │ Avoid When │
│ ────────────────────────────── │ ────────────────────── │
│ Collection utilities (Map) │ Only 2-3 types needed │
│ Data structures (Stack, Queue) │ Interfaces suffice │
│ Type-safe Result/Option types │ Readability suffers │
│ Sorting, searching algorithms │ Reflection needed │
└─────────────────────────────────────────────────────────┘
7. Testing
7.1 Table-Driven Tests
The idiomatic Go testing pattern:
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -1, -2, -3},
{"zero", 0, 0, 0},
{"mixed", -1, 5, 4},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d, want %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}
7.2 HTTP Tests
func TestGetUserHandler(t *testing.T) {
handler := http.HandlerFunc(GetUserHandler)
req := httptest.NewRequest("GET", "/users/123", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
var user User
err := json.NewDecoder(rec.Body).Decode(&user)
require.NoError(t, err)
assert.Equal(t, "123", user.ID)
}
7.3 Benchmarks and Fuzzing
// Benchmark
func BenchmarkFibonacci(b *testing.B) {
for i := 0; i < b.N; i++ {
Fibonacci(20)
}
}
// Fuzzing (Go 1.18+)
func FuzzParseJSON(f *testing.F) {
f.Add([]byte(`{"name": "test"}`))
f.Add([]byte(`{}`))
f.Fuzz(func(t *testing.T, data []byte) {
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
return
}
_, err := json.Marshal(result)
if err != nil {
t.Errorf("re-marshal failed: %v", err)
}
})
}
# Running tests
go test ./... # all tests
go test -v -run TestUser ./pkg/user # specific test
go test -bench=. ./... # benchmarks
go test -fuzz=FuzzParseJSON ./... # fuzzing
go test -race ./... # race detector
go test -cover ./... # coverage
8. Building REST APIs
8.1 Chi Router + Middleware
package main
import (
"encoding/json"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
type Server struct {
router *chi.Mux
db *sql.DB
}
func NewServer(db *sql.DB) *Server {
s := &Server{
router: chi.NewRouter(),
db: db,
}
s.routes()
return s
}
func (s *Server) routes() {
s.router.Use(middleware.Logger)
s.router.Use(middleware.Recoverer)
s.router.Use(middleware.Timeout(30 * time.Second))
s.router.Route("/api/v1", func(r chi.Router) {
r.Get("/health", s.handleHealth)
r.Route("/users", func(r chi.Router) {
r.Get("/", s.handleListUsers)
r.Post("/", s.handleCreateUser)
r.Route("/{userID}", func(r chi.Router) {
r.Get("/", s.handleGetUser)
r.Put("/", s.handleUpdateUser)
r.Delete("/", s.handleDeleteUser)
})
})
})
}
func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) {
var input CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
respondError(w, http.StatusBadRequest, "invalid request body")
return
}
user, err := s.userService.Create(r.Context(), input)
if err != nil {
respondError(w, http.StatusInternalServerError, err.Error())
return
}
respondJSON(w, http.StatusCreated, user)
}
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func respondError(w http.ResponseWriter, status int, message string) {
respondJSON(w, status, map[string]string{"error": message})
}
9. Building gRPC Services
9.1 Protocol Buffers Definition
syntax = "proto3";
package user.v1;
option go_package = "gen/user/v1;userv1";
service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse);
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
rpc StreamUpdates(StreamUpdatesRequest) returns (stream UserEvent);
}
message User {
string id = 1;
string name = 2;
string email = 3;
int64 created_at = 4;
}
9.2 gRPC Server Implementation
type userServer struct {
userv1.UnimplementedUserServiceServer
repo UserRepository
}
func (s *userServer) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) {
user, err := s.repo.GetByID(ctx, req.Id)
if err != nil {
if errors.Is(err, ErrNotFound) {
return nil, status.Error(codes.NotFound, "user not found")
}
return nil, status.Error(codes.Internal, "internal error")
}
return &userv1.GetUserResponse{User: toProtoUser(user)}, nil
}
// Server streaming
func (s *userServer) StreamUpdates(req *userv1.StreamUpdatesRequest, stream userv1.UserService_StreamUpdatesServer) error {
for event := range s.eventCh {
if err := stream.Send(event); err != nil {
return err
}
}
return nil
}
// Interceptor (middleware)
func loggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
start := time.Now()
resp, err := handler(ctx, req)
log.Printf("method=%s duration=%s error=%v", info.FullMethod, time.Since(start), err)
return resp, err
}
10. CLI Tool Development
10.1 Cobra + Viper
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var rootCmd = &cobra.Command{
Use: "mytool",
Short: "A powerful CLI tool",
Long: "mytool is a CLI application for managing deployments",
}
var deployCmd = &cobra.Command{
Use: "deploy [environment]",
Short: "Deploy application",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
env := args[0]
image, _ := cmd.Flags().GetString("image")
replicas, _ := cmd.Flags().GetInt("replicas")
dryRun, _ := cmd.Flags().GetBool("dry-run")
fmt.Printf("Deploying to %s: image=%s, replicas=%d, dry-run=%v\n",
env, image, replicas, dryRun)
if dryRun {
fmt.Println("Dry run mode - no changes applied")
return nil
}
return executeDeploy(env, image, replicas)
},
}
func init() {
deployCmd.Flags().StringP("image", "i", "", "Container image (required)")
deployCmd.Flags().IntP("replicas", "r", 1, "Number of replicas")
deployCmd.Flags().Bool("dry-run", false, "Dry run mode")
deployCmd.MarkFlagRequired("image")
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
viper.AutomaticEnv()
rootCmd.AddCommand(deployCmd)
}
11. Production Deployment
11.1 Docker Multi-Stage Build (5MB Binary)
# Build stage
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-s -w" -o /app/server ./cmd/server
# Run stage (scratch = empty image)
FROM scratch
COPY /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
# Build result size comparison
# Go scratch: ~5-10MB
# Go alpine: ~15MB
# Node.js: ~200MB+
# Java Spring: ~300MB+
# Python: ~150MB+
11.2 Graceful Shutdown
func main() {
srv := &http.Server{
Addr: ":8080",
Handler: router,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
log.Printf("Server starting on :8080")
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("Server error: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down gracefully...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("Forced shutdown: %v", err)
}
log.Println("Server stopped")
}
11.3 pprof Profiling
import _ "net/http/pprof"
func main() {
go func() {
log.Println(http.ListenAndServe(":6060", nil))
}()
// main server ...
}
# CPU profile
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# Memory profile
go tool pprof http://localhost:6060/debug/pprof/heap
# Goroutine profile
go tool pprof http://localhost:6060/debug/pprof/goroutine
12. Go vs Rust vs Java Comparison
┌─────────────────┬──────────────────┬──────────────────┬──────────────────┐
│ Category │ Go │ Rust │ Java │
├─────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ Learning Curve │ Low (1-2 weeks) │ High (3-6 months)│ Medium (1-2 mo) │
│ Compile Speed │ Very fast │ Slow │ Moderate │
│ Runtime Speed │ Fast │ Very fast │ Fast (JIT) │
│ Memory Mgmt │ GC │ Ownership system │ GC │
│ Concurrency │ Goroutines/Chan │ async/tokio │ Virtual Threads │
│ Binary Size │ 5-15MB │ 1-5MB │ 100MB+ (JRE) │
│ Ecosystem │ Cloud/Infra │ Systems/WASM │ Enterprise │
│ Error Handling │ Value return │ Result/Option │ Exceptions │
│ Generics │ 1.18+ (basic) │ Powerful │ Full support │
│ Primary Use │ Microservices, │ OS, game engines,│ Large enterprise,│
│ │ CLI, DevOps │ embedded │ Android │
│ Adoption │ Google, Uber │ Mozilla, AWS │ Most enterprises │
│ Salary (US) │ High (top 5) │ Very high (#1) │ Average │
└─────────────────┴──────────────────┴──────────────────┴──────────────────┘
Go excels at: Microservices, API servers, CLI tools, DevOps tooling, network programming Rust excels at: Systems programming, game engines, embedded, maximum performance needs Java excels at: Large enterprise, Android apps, legacy system maintenance
13. Interview Questions (15)
Fundamentals
-
What is the difference between slices and arrays in Go? Arrays have fixed size as part of the type (e.g.,
[5]int). Slices are dynamic-sized views over arrays, internally holding a pointer, length, and capacity. -
Why does Go not have inheritance? How do you reuse code? Go uses struct embedding (composition) instead of inheritance. It enforces the "composition over inheritance" principle at the language level.
-
What is the difference between a nil interface and an interface with a nil pointer? An interface is a (type, value) pair. Both nil means nil interface. Having a type but nil value means non-nil interface.
-
What is the execution order of defer, and what are the gotchas? LIFO (last in, first out). Arguments are captured at defer time, so be careful with defer in loops.
-
Explain Go's zero value philosophy. Every variable gets a valid default value on declaration, preventing uninitialized variable bugs. This reduces the need for constructors.
Concurrency
-
Why are goroutines lighter than OS threads? Goroutines start with ~2KB stack (OS threads use 1MB+), and Go's M:N scheduler maps thousands of goroutines to few OS threads.
-
Buffered vs unbuffered channels? Unbuffered channels require sender and receiver to meet synchronously. Buffered channels allow asynchronous sends up to capacity.
-
When do goroutine leaks happen and how to prevent them? When there is no counterpart to send/receive on a channel, or when context cancellation is not handled. Using context.WithCancel/Timeout is key.
-
What are deadlock conditions and how does Go detect them? Go runtime detects when all goroutines are blocked and panics. go vet and the race detector also help.
-
When to use sync.Mutex vs sync.RWMutex? RWMutex is better when reads vastly outnumber writes, as multiple goroutines can read simultaneously.
Advanced
-
What are the 3 main uses of context.Context? Cancellation propagation (Cancel), timeouts (Timeout/Deadline), value passing (WithValue). Should always be used with HTTP handlers and DB calls.
-
What are the characteristics of Go's GC? Go uses a concurrent mark-and-sweep GC with sub-millisecond STW pauses. Tunable via GOGC environment variable.
-
Explain code differences before and after generics were introduced. Before: required interface with type assertions or code generation. After: type parameters provide compile-time type safety.
-
How do you do dependency injection in Go? Typically through constructor functions that accept interfaces. DI frameworks like wire (Google) and fx (Uber) are also available.
-
How does Go module handle indirect dependencies? The indirect comment in go.mod marks dependencies not directly imported. Use
go mod tidyto clean up andgo mod vendorfor vendoring.
14. Practice Quiz
Q1: What is the output of this code?
func main() {
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i)
}()
}
time.Sleep(time.Second)
}
Answer: Before Go 1.22, mostly prints 3, 3, 3. The closure references loop variable i, which is already 3 when goroutines execute. From Go 1.22, loop variables are created fresh each iteration, outputting 0, 1, 2 (order undefined).
Q2: Why does this code deadlock?
func main() {
ch := make(chan int)
ch <- 42
fmt.Println(<-ch)
}
Answer: Sending to an unbuffered channel blocks until a receiver arrives. The main goroutine blocks at ch <- 42 and can never reach <-ch, causing deadlock. Fix: send in a separate goroutine or use a buffered channel.
Q3: Nil interface problem - why is err != nil true?
type MyError struct{}
func (e *MyError) Error() string { return "error" }
func doSomething() error {
var err *MyError = nil
return err
}
func main() {
err := doSomething()
fmt.Println(err == nil) // false!
}
Answer: doSomething() returns an interface with (type=*MyError, value=nil). Since the interface has a type, it is not nil. Fix: explicitly return nil from the function.
Q4: Common WaitGroup mistake?
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
go func(id int) {
wg.Add(1) // wrong position!
defer wg.Done()
fmt.Println(id)
}(i)
}
wg.Wait()
}
Answer: wg.Add(1) is inside the goroutine, so wg.Wait() may execute before any Add. The correct approach is to call wg.Add(1) before starting the goroutine.
Q5: Explain how context cancellation propagates.
Answer: context.WithCancel creates a child context that monitors the parent's Done channel. When the parent is cancelled, the child's Done channel also closes. This chain propagates to the top, allowing a single cancellation to clean up an entire request tree. Goroutines should check ctx.Done() in select statements to terminate properly.
References
- Go Official Documentation
- Effective Go
- Go by Example
- Go Concurrency Patterns (Rob Pike)
- Advanced Go Concurrency Patterns
- The Go Programming Language (Donovan & Kernighan)
- Concurrency in Go (Katherine Cox-Buday)
- Go Wiki: Table Driven Tests
- Go Wiki: Code Review Comments
- Uber Go Style Guide
- Go Module Reference
- Go Generics Tutorial
- pprof Guide
- Chi Router
- gRPC Go Official Guide
- Cobra CLI Library
- BubbleTea TUI Framework