✍️ 필사 모드: The Complete Go Programming Guide — Goroutines, Channels, Interfaces, and Practical Patterns
English- 1. Why Go
- 2. Basic Syntax
- 3. Goroutines and Channels
- 4. Interfaces
- 5. Error Handling
- 6. Generics (Go 1.18+)
- 7. Testing
- 8. Packages and Modules
- 9. Practical Patterns
- 10. Go in 2026
- Conclusion
1. Why Go
Go (Golang) was created at Google in 2009. Its design philosophy centers on simplicity, fast compilation, and powerful concurrency.
Why Go Gets Chosen
- Docker, Kubernetes, Terraform, Prometheus — the core tools of cloud infrastructure are all written in Go
- Compilation speed is extremely fast. Projects with hundreds of thousands of lines build in seconds
- It produces static binaries, making deployment simple. No separate runtime needed
- Goroutines handle hundreds of thousands of concurrent tasks efficiently
- Garbage collection exists but with very low latency
Go vs Other Languages
| Aspect | Go | Python | Java | Rust |
|---|---|---|---|---|
| Compile Speed | Very Fast | Interpreted | Slow | Slow |
| Execution Speed | Fast | Slow | Fast | Very Fast |
| Concurrency | Goroutines | asyncio | Threads | async/tokio |
| Learning Curve | Low | Very Low | High | Very High |
| Memory Mgmt | GC | GC | GC | Ownership |
Go sits at the optimal balance point between productivity and performance. It is not as fast as Rust, but entire teams can learn and maintain it quickly.
2. Basic Syntax
Variables and Types
package main
import "fmt"
func main() {
// Explicit declaration
var name string = "Gopher"
var age int = 10
// Short declaration (type inference)
language := "Go"
version := 1.22
// Constants
const pi = 3.14159
fmt.Printf("%s is %d years old and uses %s %.2f\n",
name, age, language, version)
fmt.Println("Pi:", pi)
}
Functions
// Basic function
func add(a, b int) int {
return a + b
}
// Multiple return values
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("cannot divide by zero")
}
return a / b, nil
}
// Named return values
func swap(a, b string) (first, second string) {
first = b
second = a
return // naked return
}
// Variadic arguments
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
Structs
type User struct {
Name string
Email string
Age int
}
// Method (value receiver)
func (u User) String() string {
return fmt.Sprintf("%s (%s)", u.Name, u.Email)
}
// Method (pointer receiver - can modify)
func (u *User) SetEmail(email string) {
u.Email = email
}
func main() {
user := User{Name: "Alice", Email: "alice@example.com", Age: 30}
user.SetEmail("new@example.com")
fmt.Println(user) // Alice (new@example.com)
}
Pointers
func main() {
x := 42
p := &x // address of x
fmt.Println(*p) // 42 (dereference)
*p = 100
fmt.Println(x) // 100
}
// Go has no pointer arithmetic - it is safe
// Unlike C, there is less worry about dangling pointers
Slices and Maps
func main() {
// Slices
nums := []int{1, 2, 3, 4, 5}
nums = append(nums, 6, 7)
sub := nums[1:4] // [2, 3, 4]
// Create with make
buffer := make([]byte, 0, 1024) // len=0, cap=1024
// Maps
scores := map[string]int{
"Alice": 95,
"Bob": 87,
}
scores["Charlie"] = 92
// Check key existence
val, ok := scores["Dave"]
if !ok {
fmt.Println("No score for Dave")
}
// Iterate map
for name, score := range scores {
fmt.Printf("%s: %d\n", name, score)
}
}
3. Goroutines and Channels
Go's concurrency model is based on CSP (Communicating Sequential Processes). The two key primitives are goroutines and channels.
Goroutine Basics
func worker(id int) {
fmt.Printf("Worker %d started\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
for i := 1; i <= 5; i++ {
go worker(i) // launch goroutine
}
// When the main goroutine ends, the program exits
time.Sleep(2 * time.Second)
}
Goroutines are not OS threads. The Go runtime multiplexes thousands of goroutines onto a small number of OS threads. Each goroutine starts with a stack of about 2KB, allowing hundreds of thousands to run concurrently.
Channel Communication
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i // send value to channel
fmt.Printf("Sent: %d\n", i)
}
close(ch) // close the channel
}
func consumer(ch <-chan int) {
for val := range ch { // receive until channel is closed
fmt.Printf("Received: %d\n", val)
}
}
func main() {
ch := make(chan int, 3) // buffered channel with capacity 3
go producer(ch)
consumer(ch) // consume in main goroutine
}
The select Statement
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("Timeout!")
}
}
}
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("Error: %s - %v\n", u, err)
return
}
defer resp.Body.Close()
fmt.Printf("%s -> %s\n", u, resp.Status)
}(url)
}
wg.Wait() // wait for all goroutines
fmt.Println("All requests complete")
}
Deadlock Prevention Patterns
// Bad: deadlock
func bad() {
ch := make(chan int) // unbuffered channel
ch <- 1 // blocks forever - no receiver
fmt.Println(<-ch)
}
// Good 1: send in a goroutine
func good1() {
ch := make(chan int)
go func() { ch <- 1 }()
fmt.Println(<-ch)
}
// Good 2: use buffered channel
func good2() {
ch := make(chan int, 1)
ch <- 1
fmt.Println(<-ch)
}
Worker Pool Pattern
func workerPool(jobs <-chan int, results chan<- int, id int) {
for j := range jobs {
fmt.Printf("Worker %d processing: 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)
// Start workers
for w := 1; w <= numWorkers; w++ {
go workerPool(jobs, results, w)
}
// Send jobs
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
// Collect results
for r := 1; r <= numJobs; r++ {
result := <-results
fmt.Printf("Result: %d\n", result)
}
}
4. Interfaces
Go interfaces are implemented implicitly. There is no implements keyword.
Basic Interface
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)
}
// Function that accepts the Shape interface
func printInfo(s Shape) {
fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}
io.Reader / io.Writer
The most powerful interfaces in the Go standard library.
// io.Reader interface
// type Reader interface {
// Read(p []byte) (n int, err error)
// }
// io.Writer interface
// 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() {
// Read from string
r := strings.NewReader("Hello, Go!")
n, _ := countBytes(r)
fmt.Printf("%d bytes\n", n) // 10 bytes
// Read from file
f, _ := os.Open("data.txt")
defer f.Close()
n, _ = countBytes(f)
fmt.Printf("%d bytes\n", n)
}
Empty Interface and Type Assertions
func describe(i interface{}) {
// Type assertion
if s, ok := i.(string); ok {
fmt.Println("String:", s)
return
}
// Type switch
switch v := i.(type) {
case int:
fmt.Println("Integer:", v)
case float64:
fmt.Println("Float:", v)
case bool:
fmt.Println("Boolean:", v)
default:
fmt.Printf("Unknown type: %T\n", v)
}
}
func main() {
describe(42)
describe("hello")
describe(3.14)
describe(true)
}
Interface Composition
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
}
// Interface composition (embedding)
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
5. Error Handling
Go uses explicit error returns instead of exceptions.
errors.New and fmt.Errorf
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
func findUser(id int) (*User, error) {
if id <= 0 {
return nil, fmt.Errorf("invalid user ID: %d", id)
}
// ... DB query
return nil, ErrNotFound
}
Error Wrapping with errors.Is / errors.As
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed - %s: %s", e.Field, e.Message)
}
func validateAge(age int) error {
if age < 0 || age > 150 {
return &ValidationError{
Field: "age",
Message: fmt.Sprintf("age must be 0-150 (got: %d)", age),
}
}
return nil
}
func processUser(age int) error {
if err := validateAge(age); err != nil {
return fmt.Errorf("user processing failed: %w", err) // wrap error
}
return nil
}
func main() {
err := processUser(-5)
// errors.Is: check for a specific error in the chain
if errors.Is(err, ErrNotFound) {
fmt.Println("User not found")
}
// errors.As: convert to a specific type in the chain
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("Field: %s, Message: %s\n", valErr.Field, valErr.Message)
}
}
Custom Error Pattern
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
}
// Error creation helper
func NewAppError(code int, msg string, err error) *AppError {
return &AppError{Code: code, Message: msg, Err: err}
}
6. Generics (Go 1.18+)
Generics were introduced in Go 1.18, enabling type-safe generic code.
Type Parameters
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
}
Constraints
import "golang.org/x/exp/constraints"
// Custom constraint
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
}
// Generics with structs
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)
}
Practical Generic Example: Map Utilities
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. Testing
Go has a powerful built-in testing framework. No external libraries are needed.
Basic Tests
// 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)
}
}
Table-Driven Tests
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)
}
})
}
}
Benchmarks
func BenchmarkFibonacci(b *testing.B) {
for i := 0; i < b.N; i++ {
Fibonacci(20)
}
}
// Run: 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("status code = %d; want %d",
resp.StatusCode, http.StatusOK)
}
body, _ := io.ReadAll(resp.Body)
if string(body) != `{"status":"ok"}` {
t.Errorf("response = %s; want {\"status\":\"ok\"}", body)
}
}
8. Packages and Modules
Initializing go mod
# Start a new project
mkdir myproject && cd myproject
go mod init github.com/username/myproject
# Add dependencies
go get github.com/gin-gonic/gin@latest
# Clean up unused dependencies
go mod tidy
# Download dependencies
go mod download
Project Structure
myproject/
cmd/
server/
main.go # Entry point
internal/
handler/
user.go # HTTP handlers
user_test.go
service/
user.go # Business logic
repository/
user.go # Data access
pkg/
validator/
validator.go # Publicly shared utilities
go.mod
go.sum
The internal Package
Packages inside the internal directory cannot be imported from outside the module. This clearly separates the public API from internal implementation.
// internal/config/config.go
package config
type Config struct {
Port int
DBHost string
LogLevel string
}
func Load() (*Config, error) {
// Load config from environment variables
return &Config{
Port: 8080,
DBHost: "localhost:5432",
LogLevel: "info",
}, nil
}
9. Practical Patterns
Web Server (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("Server starting: %s", server.Addr)
log.Fatal(server.ListenAndServe())
}
Middleware
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("[%s] %s started", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
log.Printf("[%s] %s completed (%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, "Authentication required", http.StatusUnauthorized)
return
}
// Token validation logic...
next.ServeHTTP(w, r)
})
}
// Middleware chaining
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,
}
// Start server in a goroutine
go func() {
log.Println("Server starting: :8080")
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("Server error: %v", err)
}
}()
// Wait for shutdown signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Server shutting down...")
// Graceful shutdown with 30s timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
log.Println("Server shutdown complete")
}
Configuration Management
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("failed to read config file: %w", err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("failed to parse config: %w", err)
}
return &cfg, nil
}
10. Go in 2026
Cloud Infrastructure
Go is the de facto standard language of the cloud-native ecosystem.
- Container Orchestration: Kubernetes, Docker, containerd
- Service Mesh: Istio, Linkerd
- Monitoring: Prometheus, Grafana Agent, Thanos
- IaC: Terraform, Pulumi
- CI/CD: Drone, Tekton
CLI Tools
CLI tools built with Go ship as a single binary, making installation effortless.
// CLI example using cobra
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "mytool",
Short: "My CLI tool",
}
var greetCmd = &cobra.Command{
Use: "greet [name]",
Short: "Say hello",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Hello, %s!\n", args[0])
},
}
func main() {
rootCmd.AddCommand(greetCmd)
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
Microservices
Go is optimized for microservice architectures.
- Fast startup time: Minimizes cold start latency
- Low memory usage: Saves container resources
- Static binary: Lightweight Docker images (scratch base possible)
- Native gRPC support: Service-to-service communication with protobuf + gRPC
- OpenTelemetry: Distributed tracing and metrics collection
# Multi-stage build
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"]
The final image size is around 10-20MB — less than 1/10 of Java or Python-based services.
Conclusion
Go is a language where simplicity is power. It excludes complex features and provides only what is essential. This philosophy is why Go is chosen across a broad range of domains, from infrastructure tools like Docker and Kubernetes to CLI tools and microservices.
The best way to start is to write code yourself. Try building a simple CLI tool or a REST API server.
Quiz: Test Your Go Concurrency Knowledge
Q1. What is the difference between a goroutine and an OS thread?
The Go runtime multiplexes goroutines onto a small number of OS threads. A goroutine starts with a stack of about 2KB, much lighter than the 1-8MB stack of an OS thread.
Q2. What is the relationship between sending and receiving on an unbuffered channel?
The sender blocks until a receiver is ready, and the receiver blocks until a sender is ready. It is synchronous communication.
Q3. What is the role of the select statement?
It waits on multiple channel operations simultaneously, processing whichever channel is ready first. It is useful for timeout and cancellation handling.
현재 단락 (1/747)
Go (Golang) was created at Google in 2009. Its design philosophy centers on **simplicity, fast compi...