Split View: Go 프로그래밍 완전 가이드 — 고루틴, 채널, 인터페이스, 실전 패턴
Go 프로그래밍 완전 가이드 — 고루틴, 채널, 인터페이스, 실전 패턴
- 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 문의 역할은?
여러 채널 연산을 동시에 대기하면서, 준비된 채널에서 먼저 처리합니다. 타임아웃이나 취소 처리에 유용합니다.
The Complete Go Programming Guide — Goroutines, Channels, Interfaces, and Practical Patterns
- 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.