🏗️ GO 구조체와 인터페이스
📋 목차
1. 구조체 기초
📦 구조체 정의와 생성
구조체란?
구조체는 서로 다른 타입의 데이터를 하나로 묶는 사용자 정의 타입입니다. Go의 객체지향 프로그래밍의 기초가 됩니다.
package main
import (
"fmt"
"time"
)
// 기본 구조체 정의
type Person struct {
Name string
Age int
Email string
IsActive bool
}
// 구조체 필드에 태그 추가 (JSON, 검증 등에 사용)
type User struct {
ID int `json:"id" validate:"required"`
Username string `json:"username" validate:"required,min=3"`
Password string `json:"-"` // JSON에서 제외
Email string `json:"email" validate:"email"`
CreatedAt time.Time `json:"created_at"`
}
// 중첩 구조체
type Address struct {
Street string
City string
ZipCode string
Country string
}
type Employee struct {
Person // 임베딩 (상속과 유사)
ID int // 직원 ID
Position string // 직책
Salary float64 // 급여
Address Address // 주소 (컴포지션)
Manager *Employee // 매니저 (자기 참조)
}
func structBasics() {
// 1. 구조체 생성 방법들
// 필드별 초기화
person1 := Person{
Name: "Alice",
Age: 30,
Email: "alice@example.com",
IsActive: true,
}
// 순서대로 초기화
person2 := Person{"Bob", 25, "bob@example.com", false}
// 빈 구조체 생성 후 필드 설정
var person3 Person
person3.Name = "Charlie"
person3.Age = 35
// new()를 사용한 포인터 생성
person4 := new(Person)
person4.Name = "Diana"
person4.Age = 28
fmt.Printf("Person1: %+v\n", person1)
fmt.Printf("Person2: %+v\n", person2)
fmt.Printf("Person3: %+v\n", person3)
fmt.Printf("Person4: %+v\n", *person4)
}🔄 구조체 조작
func structOperations() {
// 구조체 생성
user := User{
ID: 1,
Username: "gopher",
Email: "gopher@golang.org",
CreatedAt: time.Now(),
}
// 필드 접근 및 수정
fmt.Printf("Original user: %+v\n", user)
user.Username = "super_gopher"
user.Email = "super_gopher@golang.org"
fmt.Printf("Modified user: %+v\n", user)
// 구조체 복사 (값 복사)
userCopy := user
userCopy.Username = "copy_gopher"
fmt.Printf("Original after copy: %+v\n", user)
fmt.Printf("Copy: %+v\n", userCopy)
// 구조체 포인터
userPtr := &user
userPtr.Email = "ptr_gopher@golang.org"
fmt.Printf("After pointer modification: %+v\n", user)
}🏢 복잡한 구조체 예제
func complexStructs() {
// 매니저 생성
manager := &Employee{
Person: Person{
Name: "John Manager",
Age: 45,
Email: "john.manager@company.com",
IsActive: true,
},
ID: 101,
Position: "Engineering Manager",
Salary: 120000.0,
Address: Address{
Street: "123 Manager St",
City: "San Francisco",
ZipCode: "94105",
Country: "USA",
},
Manager: nil, // 최고 관리자
}
// 직원 생성
employee := Employee{
Person: Person{
Name: "Jane Developer",
Age: 28,
Email: "jane.dev@company.com",
IsActive: true,
},
ID: 201,
Position: "Software Engineer",
Salary: 85000.0,
Address: Address{
Street: "456 Dev Ave",
City: "San Francisco",
ZipCode: "94107",
Country: "USA",
},
Manager: manager, // 매니저 할당
}
// 정보 출력
fmt.Printf("Employee: %s\n", employee.Name) // 임베딩된 필드 직접 접근
fmt.Printf("Position: %s\n", employee.Position)
fmt.Printf("Manager: %s\n", employee.Manager.Name)
fmt.Printf("Address: %s, %s\n", employee.Address.City, employee.Address.Country)
}2. 메서드
🎯 메서드 정의
메서드란?
메서드는 특정 타입에 연결된 함수입니다. 리시버(receiver)를 통해 구조체와 연결됩니다.
import (
"fmt"
"math"
)
type Circle struct {
X, Y float64 // 중심 좌표
Radius float64 // 반지름
}
type Rectangle struct {
Width, Height float64
}
// 값 리시버 메서드
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Circumference() float64 {
return 2 * math.Pi * c.Radius
}
// 포인터 리시버 메서드 (원본 수정 가능)
func (c *Circle) Scale(factor float64) {
c.Radius *= factor
}
func (c *Circle) MoveTo(x, y float64) {
c.X = x
c.Y = y
}
// String() 메서드 (fmt 패키지가 자동으로 호출)
func (c Circle) String() string {
return fmt.Sprintf("Circle(center: %.1f,%.1f, radius: %.1f)",
c.X, c.Y, c.Radius)
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
func methodBasics() {
circle := Circle{X: 0, Y: 0, Radius: 5}
// 값 리시버 메서드 호출
fmt.Printf("Circle area: %.2f\n", circle.Area())
fmt.Printf("Circle circumference: %.2f\n", circle.Circumference())
// 포인터 리시버 메서드 호출
fmt.Printf("Before scaling: %v\n", circle)
circle.Scale(2.0) // Go가 자동으로 &circle.Scale(2.0)로 변환
fmt.Printf("After scaling: %v\n", circle)
circle.MoveTo(10, 10)
fmt.Printf("After moving: %v\n", circle)
// Rectangle
rect := Rectangle{Width: 10, Height: 20}
fmt.Printf("Rectangle area: %.2f\n", rect.Area())
fmt.Printf("Rectangle perimeter: %.2f\n", rect.Perimeter())
}🔄 값 리시버 vs 포인터 리시버
type Counter struct {
value int
}
// 값 리시버 - 복사본에서 작업 (원본 변경 안됨)
func (c Counter) IncrementValue() {
c.value++ // 복사본만 변경됨
}
// 포인터 리시버 - 원본에서 작업 (원본 변경됨)
func (c *Counter) IncrementPointer() {
c.value++ // 원본이 변경됨
}
func (c Counter) GetValue() int {
return c.value
}
// 포인터 리시버로 String 구현
func (c *Counter) String() string {
return fmt.Sprintf("Counter{value: %d}", c.value)
}
func receiverTypes() {
counter := Counter{value: 0}
fmt.Printf("Initial: %v\n", counter)
// 값 리시버 호출 - 원본 변경 안됨
counter.IncrementValue()
fmt.Printf("After IncrementValue: %v\n", counter)
// 포인터 리시버 호출 - 원본 변경됨
counter.IncrementPointer()
fmt.Printf("After IncrementPointer: %v\n", counter)
// 포인터를 통해서도 호출 가능
counterPtr := &counter
counterPtr.IncrementPointer()
fmt.Printf("After pointer increment: %v\n", counter)
}3. 인터페이스
🎭 인터페이스 기초
Go의 인터페이스
Go의 인터페이스는 암시적입니다. 명시적으로 구현을 선언할 필요 없이, 메서드만 구현하면 자동으로 인터페이스를 만족합니다.
// Shape 인터페이스 정의
type Shape interface {
Area() float64
Perimeter() float64
}
// Drawable 인터페이스
type Drawable interface {
Draw() string
}
// 복합 인터페이스 (인터페이스 임베딩)
type DrawableShape interface {
Shape // Shape 인터페이스 임베딩
Drawable // Drawable 인터페이스 임베딩
}
// Circle의 Drawable 인터페이스 구현
func (c Circle) Perimeter() float64 {
return c.Circumference() // 기존 메서드 재사용
}
func (c Circle) Draw() string {
return fmt.Sprintf("Drawing a circle with radius %.1f at (%.1f, %.1f)",
c.Radius, c.X, c.Y)
}
// Rectangle의 Drawable 인터페이스 구현
func (r Rectangle) Draw() string {
return fmt.Sprintf("Drawing a rectangle %.1fx%.1f", r.Width, r.Height)
}
// Triangle 새로운 도형
type Triangle struct {
A, B, C float64 // 세 변의 길이
}
func (t Triangle) Area() float64 {
s := (t.A + t.B + t.C) / 2 // 반둘레
return math.Sqrt(s * (s - t.A) * (s - t.B) * (s - t.C)) // 헤론의 공식
}
func (t Triangle) Perimeter() float64 {
return t.A + t.B + t.C
}
func (t Triangle) Draw() string {
return fmt.Sprintf("Drawing a triangle with sides %.1f, %.1f, %.1f",
t.A, t.B, t.C)
}
func interfaceBasics() {
// 다양한 도형 생성
shapes := []Shape{
Circle{Radius: 5},
Rectangle{Width: 10, Height: 20},
Triangle{A: 3, B: 4, C: 5},
}
// 인터페이스를 통한 폴리모피즘
for i, shape := range shapes {
fmt.Printf("Shape %d:\n", i+1)
fmt.Printf(" Area: %.2f\n", shape.Area())
fmt.Printf(" Perimeter: %.2f\n", shape.Perimeter())
// 타입 assertion을 통한 구체 타입 확인
switch s := shape.(type) {
case Circle:
fmt.Printf(" Type: Circle (radius: %.1f)\n", s.Radius)
case Rectangle:
fmt.Printf(" Type: Rectangle (%.1fx%.1f)\n", s.Width, s.Height)
case Triangle:
fmt.Printf(" Type: Triangle (sides: %.1f, %.1f, %.1f)\n", s.A, s.B, s.C)
}
// Drawable 인터페이스 체크
if drawable, ok := shape.(Drawable); ok {
fmt.Printf(" %s\n", drawable.Draw())
}
fmt.Println()
}
}🔍 인터페이스 고급 활용
import (
"io"
"strings"
"encoding/json"
)
// 빈 인터페이스 (모든 타입을 담을 수 있음)
func handleAny(value interface{}) {
fmt.Printf("Value: %v, Type: %T\n", value, value)
// 타입 스위치
switch v := value.(type) {
case int:
fmt.Printf(" Integer: %d\n", v)
case string:
fmt.Printf(" String: %s\n", v)
case []int:
fmt.Printf(" Int slice: %v\n", v)
case Shape:
fmt.Printf(" Shape with area: %.2f\n", v.Area())
default:
fmt.Printf(" Unknown type: %T\n", v)
}
}
// 인터페이스 조합
type Writer interface {
Write([]byte) (int, error)
}
type Reader interface {
Read([]byte) (int, error)
}
// io.ReadWriter는 Reader와 Writer를 모두 구현
type ReadWriter interface {
Reader
Writer
}
// 커스텀 Writer 구현
type StringWriter struct {
data strings.Builder
}
func (sw *StringWriter) Write(p []byte) (n int, err error) {
return sw.data.Write(p)
}
func (sw *StringWriter) String() string {
return sw.data.String()
}
func interfaceAdvanced() {
// 빈 인터페이스 사용
values := []interface{}{
42,
"Hello",
[]int{1, 2, 3},
Circle{Radius: 3},
Rectangle{Width: 5, Height: 10},
}
for _, val := range values {
handleAny(val)
fmt.Println()
}
// Writer 인터페이스 사용
var writer StringWriter
// io.WriteString은 Writer 인터페이스를 받음
io.WriteString(&writer, "Hello, ")
io.WriteString(&writer, "Go Interface!")
fmt.Printf("Writer result: %s\n", writer.String())
}🎯 인터페이스 실전 패턴
// Strategy 패턴
type PaymentProcessor interface {
ProcessPayment(amount float64) error
GetFee(amount float64) float64
}
type CreditCardProcessor struct {
CardNumber string
}
func (cc CreditCardProcessor) ProcessPayment(amount float64) error {
fmt.Printf("Processing credit card payment: $%.2f\n", amount)
return nil
}
func (cc CreditCardProcessor) GetFee(amount float64) float64 {
return amount * 0.03 // 3% 수수료
}
type PayPalProcessor struct {
Email string
}
func (pp PayPalProcessor) ProcessPayment(amount float64) error {
fmt.Printf("Processing PayPal payment: $%.2f\n", amount)
return nil
}
func (pp PayPalProcessor) GetFee(amount float64) float64 {
return amount * 0.025 // 2.5% 수수료
}
// 결제 서비스
type PaymentService struct {
processor PaymentProcessor
}
func NewPaymentService(processor PaymentProcessor) *PaymentService {
return &PaymentService{processor: processor}
}
func (ps *PaymentService) ProcessOrder(amount float64) error {
fee := ps.processor.GetFee(amount)
totalAmount := amount + fee
fmt.Printf("Order amount: $%.2f, Fee: $%.2f, Total: $%.2f\n",
amount, fee, totalAmount)
return ps.processor.ProcessPayment(totalAmount)
}
func strategyPattern() {
// 신용카드 결제
ccProcessor := CreditCardProcessor{CardNumber: "**** **** **** 1234"}
ccService := NewPaymentService(ccProcessor)
ccService.ProcessOrder(100.00)
fmt.Println()
// PayPal 결제
ppProcessor := PayPalProcessor{Email: "user@example.com"}
ppService := NewPaymentService(ppProcessor)
ppService.ProcessOrder(100.00)
}4. 임베딩과 컴포지션
🧩 구조체 임베딩
임베딩
Go는 상속을 지원하지 않지만, 구조체 임베딩을 통해 유사한 효과를 얻을 수 있습니다.
// 기본 엔티티
type BaseEntity struct {
ID int
CreatedAt time.Time
UpdatedAt time.Time
}
func (be *BaseEntity) SetID(id int) {
be.ID = id
}
func (be *BaseEntity) GetID() int {
return be.ID
}
func (be *BaseEntity) Touch() {
be.UpdatedAt = time.Now()
}
// 사용자 엔티티 (BaseEntity 임베딩)
type User struct {
BaseEntity // 임베딩 (익명 필드)
Username string
Email string
IsActive bool
}
func (u *User) Activate() {
u.IsActive = true
u.Touch() // 임베딩된 메서드 호출
}
func (u *User) Deactivate() {
u.IsActive = false
u.Touch()
}
// 제품 엔티티 (BaseEntity 임베딩)
type Product struct {
BaseEntity
Name string
Price float64
InStock bool
Description string
}
func (p *Product) UpdatePrice(newPrice float64) {
p.Price = newPrice
p.Touch() // 임베딩된 메서드 호출
}
func embeddingExample() {
// 사용자 생성
user := User{
BaseEntity: BaseEntity{
ID: 1,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
Username: "gopher",
Email: "gopher@example.com",
IsActive: false,
}
fmt.Printf("User before activation: %+v\n", user)
// 임베딩된 메서드와 추가된 메서드 사용
user.Activate()
fmt.Printf("User after activation: %+v\n", user)
// 임베딩된 필드 직접 접근
fmt.Printf("User ID: %d\n", user.ID) // user.BaseEntity.ID와 같음
fmt.Printf("User ID (method): %d\n", user.GetID())
// 제품 생성
product := Product{
BaseEntity: BaseEntity{
ID: 101,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
Name: "Go Programming Book",
Price: 29.99,
InStock: true,
Description: "Learn Go programming",
}
fmt.Printf("\nProduct before price update: %+v\n", product)
product.UpdatePrice(24.99)
fmt.Printf("Product after price update: %+v\n", product)
}🔄 메서드 오버라이드
// 로깅 기능이 있는 기본 구조체
type Logger struct {
Name string
}
func (l Logger) Log(message string) {
fmt.Printf("[%s] %s\n", l.Name, message)
}
func (l Logger) Info(message string) {
l.Log("INFO: " + message)
}
// 데이터베이스 로거 (Logger 임베딩)
type DatabaseLogger struct {
Logger
Connection string
}
// Log 메서드 오버라이드
func (dl DatabaseLogger) Log(message string) {
fmt.Printf("[DB:%s] [%s] %s\n", dl.Connection, dl.Name, message)
}
// 파일 로거 (Logger 임베딩)
type FileLogger struct {
Logger
FilePath string
}
// Log 메서드 오버라이드
func (fl FileLogger) Log(message string) {
fmt.Printf("[FILE:%s] [%s] %s\n", fl.FilePath, fl.Name, message)
}
func methodOverride() {
// 기본 로거
basicLogger := Logger{Name: "BasicLogger"}
basicLogger.Log("Basic log message")
basicLogger.Info("Basic info message")
fmt.Println()
// 데이터베이스 로거
dbLogger := DatabaseLogger{
Logger: Logger{Name: "DBLogger"},
Connection: "postgresql://localhost:5432",
}
dbLogger.Log("Database log message") // 오버라이드된 메서드
dbLogger.Info("Database info message") // 임베딩된 메서드가 오버라이드된 Log 호출
fmt.Println()
// 파일 로거
fileLogger := FileLogger{
Logger: Logger{Name: "FileLogger"},
FilePath: "/var/log/app.log",
}
fileLogger.Log("File log message") // 오버라이드된 메서드
fileLogger.Info("File info message") // 임베딩된 메서드가 오버라이드된 Log 호출
}🎯 실습 예제
📝 연습 문제 1: 은행 계좌 시스템
package main
import (
"fmt"
"errors"
"time"
)
// 계좌 인터페이스
type Account interface {
Deposit(amount float64) error
Withdraw(amount float64) error
GetBalance() float64
GetAccountInfo() string
}
// 기본 계좌
type BaseAccount struct {
AccountNumber string
Owner string
Balance float64
CreatedAt time.Time
}
func (ba *BaseAccount) Deposit(amount float64) error {
if amount <= 0 {
return errors.New("deposit amount must be positive")
}
ba.Balance += amount
return nil
}
func (ba *BaseAccount) GetBalance() float64 {
return ba.Balance
}
func (ba *BaseAccount) GetAccountInfo() string {
return fmt.Sprintf("Account: %s, Owner: %s, Balance: $%.2f",
ba.AccountNumber, ba.Owner, ba.Balance)
}
// 저축 계좌 (BaseAccount 임베딩)
type SavingsAccount struct {
BaseAccount
InterestRate float64
}
func (sa *SavingsAccount) Withdraw(amount float64) error {
if amount <= 0 {
return errors.New("withdrawal amount must be positive")
}
if sa.Balance < amount {
return errors.New("insufficient funds")
}
sa.Balance -= amount
return nil
}
func (sa *SavingsAccount) AddInterest() {
interest := sa.Balance * sa.InterestRate / 100
sa.Balance += interest
fmt.Printf("Interest added: $%.2f\n", interest)
}
// 당좌 계좌 (BaseAccount 임베딩)
type CheckingAccount struct {
BaseAccount
OverdraftLimit float64
}
func (ca *CheckingAccount) Withdraw(amount float64) error {
if amount <= 0 {
return errors.New("withdrawal amount must be positive")
}
if sa.Balance + ca.OverdraftLimit < amount {
return errors.New("overdraft limit exceeded")
}
ca.Balance -= amount
return nil
}
func (ca *CheckingAccount) GetOverdraftUsage() float64 {
if ca.Balance >= 0 {
return 0
}
return -ca.Balance
}
// 은행 시스템
type Bank struct {
accounts map[string]Account
}
func NewBank() *Bank {
return &Bank{
accounts: make(map[string]Account),
}
}
func (b *Bank) AddAccount(account Account) {
accountInfo := account.GetAccountInfo()
// 계좌 번호 추출 (실제로는 더 정교한 방법 필요)
b.accounts[accountInfo] = account
}
func (b *Bank) ProcessTransaction(account Account, transactionType string, amount float64) {
fmt.Printf("\n--- %s Transaction ---\n", transactionType)
fmt.Printf("Before: %s\n", account.GetAccountInfo())
var err error
switch transactionType {
case "Deposit":
err = account.Deposit(amount)
case "Withdraw":
err = account.Withdraw(amount)
}
if err != nil {
fmt.Printf("Transaction failed: %v\n", err)
} else {
fmt.Printf("Transaction successful: $%.2f\n", amount)
}
fmt.Printf("After: %s\n", account.GetAccountInfo())
}
func bankSystemExample() {
bank := NewBank()
// 저축 계좌 생성
savings := &SavingsAccount{
BaseAccount: BaseAccount{
AccountNumber: "SAV001",
Owner: "Alice",
Balance: 1000.0,
CreatedAt: time.Now(),
},
InterestRate: 2.5,
}
// 당좌 계좌 생성
checking := &CheckingAccount{
BaseAccount: BaseAccount{
AccountNumber: "CHK001",
Owner: "Bob",
Balance: 500.0,
CreatedAt: time.Now(),
},
OverdraftLimit: 200.0,
}
bank.AddAccount(savings)
bank.AddAccount(checking)
// 거래 처리
bank.ProcessTransaction(savings, "Deposit", 200.0)
bank.ProcessTransaction(savings, "Withdraw", 150.0)
bank.ProcessTransaction(checking, "Deposit", 100.0)
bank.ProcessTransaction(checking, "Withdraw", 800.0) // 한도 초과 테스트
bank.ProcessTransaction(checking, "Withdraw", 650.0) // 성공
// 이자 추가 (저축 계좌만)
fmt.Println("\n--- Interest Calculation ---")
savings.AddInterest()
fmt.Printf("After interest: %s\n", savings.GetAccountInfo())
// 오버드래프트 사용량 확인 (당좌 계좌만)
fmt.Printf("Overdraft usage: $%.2f\n", checking.GetOverdraftUsage())
}📝 연습 문제 2: 게임 캐릭터 시스템
// 캐릭터 인터페이스
type Character interface {
Attack() int
TakeDamage(damage int)
IsAlive() bool
GetStatus() string
}
// 기본 캐릭터
type BaseCharacter struct {
Name string
HP int
MaxHP int
AttackPower int
}
func (bc *BaseCharacter) TakeDamage(damage int) {
bc.HP -= damage
if bc.HP < 0 {
bc.HP = 0
}
}
func (bc *BaseCharacter) IsAlive() bool {
return bc.HP > 0
}
func (bc *BaseCharacter) GetStatus() string {
return fmt.Sprintf("%s: HP %d/%d", bc.Name, bc.HP, bc.MaxHP)
}
// 전사 (BaseCharacter 임베딩)
type Warrior struct {
BaseCharacter
Armor int
}
func (w *Warrior) Attack() int {
return w.AttackPower + 5 // 전사는 추가 공격력
}
func (w *Warrior) TakeDamage(damage int) {
reducedDamage := damage - w.Armor
if reducedDamage < 0 {
reducedDamage = 0
}
w.BaseCharacter.TakeDamage(reducedDamage)
}
// 마법사 (BaseCharacter 임베딩)
type Mage struct {
BaseCharacter
Mana int
MaxMana int
}
func (m *Mage) Attack() int {
return w.AttackPower
}
func (m *Mage) CastSpell() int {
if m.Mana >= 10 {
m.Mana -= 10
return m.AttackPower * 2 // 마법은 2배 데미지
}
return m.Attack() // 마나가 없으면 일반 공격
}
func gameExample() {
warrior := &Warrior{
BaseCharacter: BaseCharacter{
Name: "Knight",
HP: 100,
MaxHP: 100,
AttackPower: 20,
},
Armor: 5,
}
mage := &Mage{
BaseCharacter: BaseCharacter{
Name: "Wizard",
HP: 70,
MaxHP: 70,
AttackPower: 15,
},
Mana: 50,
MaxMana: 50,
}
// 전투 시뮬레이션
fmt.Println("=== Battle Start ===")
fmt.Printf("%s vs %s\n", warrior.Name, mage.Name)
round := 1
for warrior.IsAlive() && mage.IsAlive() {
fmt.Printf("\n--- Round %d ---\n", round)
// 전사 공격
if warrior.IsAlive() {
damage := warrior.Attack()
mage.TakeDamage(damage)
fmt.Printf("%s attacks for %d damage\n", warrior.Name, damage)
fmt.Printf("%s\n", mage.GetStatus())
}
// 마법사 공격
if mage.IsAlive() {
damage := mage.CastSpell()
warrior.TakeDamage(damage)
fmt.Printf("%s casts spell for %d damage\n", mage.Name, damage)
fmt.Printf("%s\n", warrior.GetStatus())
}
round++
if round > 10 { // 무한 루프 방지
break
}
}
fmt.Println("\n=== Battle End ===")
if warrior.IsAlive() {
fmt.Printf("%s wins!\n", warrior.Name)
} else if mage.IsAlive() {
fmt.Printf("%s wins!\n", mage.Name)
} else {
fmt.Println("Draw!")
}
}✅ 체크리스트
구조체
- 구조체 정의와 초기화
- 구조체 태그 활용
- 중첩 구조체와 자기 참조
- 구조체 복사와 포인터
메서드
- 값 리시버 vs 포인터 리시버
- 메서드 정의와 호출
- String() 메서드 구현
- 메서드 체이닝 패턴
인터페이스
- 인터페이스 정의와 구현
- 암시적 인터페이스 이해
- 빈 인터페이스 활용
- 타입 assertion과 타입 스위치
임베딩과 컴포지션
- 구조체 임베딩 이해
- 메서드 오버라이드
- 컴포지션 vs 상속
- 인터페이스 임베딩
다음 단계
구조체와 인터페이스를 익혔다면 GO 동시성 - 고루틴과 채널로 넘어가세요!