Skip to content

บทที่ 5 — Structs และ Methods

← บทที่ 4 | สารบัญ | บทที่ 6 →

หลังจบบท คุณจะ:

  • สร้าง struct ที่ idiomatic Go
  • เลือกใช้ value receiver vs pointer receiver
  • ใช้ embedding — วิธีที่ Go ใช้แทน inheritance (การสืบทอด) — รายละเอียดเต็มอยู่ข้อ 9
  • จัดการ visibility ผ่าน capitalization

1. Struct — Group ของ Field

Struct คือวิธีจับกลุ่มข้อมูลที่เกี่ยวข้องกันไว้เป็นก้อนเดียว (เช่น ชื่อ + อีเมล + อายุ = User หนึ่งคน) — เป็นรากฐานของการจัดโครงสร้างข้อมูลใน Go (แทนที่ class ในภาษา OOP) เราสร้างได้หลายวิธีและทุก field มี zero value ให้เสมอ:

⚠️ ระวัง (สำหรับ dev .NET): Go struct มี value semantics เหมือน C# struct (ก๊อปข้อมูลเมื่อ assign หรือส่งเข้าฟังก์ชัน) ไม่ใช่ C# class (reference type) — ถ้าอยากได้พฤติกรรมแบบ C# class ต้องใช้ *User (pointer) เสมอ

go
type User struct {
    Name  string
    Email string
    Age   int
}

// Initialize
u1 := User{Name: "Anna", Email: "anna@x.com", Age: 25}     // named fields
u2 := User{"Anna", "anna@x.com", 25}                        // positional (ไม่แนะนำ)
u3 := User{Name: "Anna"}                                     // partial — other = zero value
u4 := User{}                                                 // all zero value

// Access
fmt.Println(u1.Name)
u1.Age = 26

// new() returns *T (pointer, all zero)
u5 := new(User)              // u5 = &User{} (pointer)
u5.Name = "Anna"             // OK — Go auto-dereference

Zero Value

go
var u User
// u = User{Name: "", Email: "", Age: 0}

→ Struct field มี zero value เสมอ — เหมือน primitive


2. Pointer to Struct

บ่อยครั้งเราทำงานกับ "pointer ที่ชี้ไป struct" (*User) แทนตัว struct เอง เพื่อหลีกเลี่ยงการก๊อปข้อมูลและเพื่อให้แก้ค่าตัวจริงได้ ข้อดีของ Go คือเขียน p.Name ได้เลย ไม่ต้อง (*p).Name เพราะ compiler ช่วย dereference ให้:

go
u := User{Name: "Anna"}
p := &u                       // *User

p.Name = "Modified"           // auto-dereference: (*p).Name
fmt.Println(u.Name)           // "Modified"
go
// Or create directly
p := &User{Name: "Anna"}      // pointer literal
go
// new keyword
p := new(User)                 // pointer to zero-value User
p.Name = "Anna"

→ ใน Go — เขียน p.Name แทน (*p).Name (compiler ช่วย)


3. Comparable Struct

struct เปรียบเทียบกันด้วย == ได้ถ้าทุก field เป็นชนิดที่เทียบได้ (Go จะเทียบทีละ field ให้) — แต่ถ้ามี field เป็น slice/map/func จะเทียบไม่ได้ (compile error) ต้องใช้ reflect.DeepEqual() หรือเขียน method Equal() เอง:

go
u1 := User{Name: "Anna", Age: 25}
u2 := User{Name: "Anna", Age: 25}

u1 == u2                       // true (compare all fields)

// Struct ที่มี slice/map/func → ไม่ comparable
type Foo struct {
    Tags []string             // slice
}

f1 := Foo{Tags: []string{"a"}}
f2 := Foo{Tags: []string{"a"}}
f1 == f2                       // ❌ compile error

→ ใช้ reflect.DeepEqual() หรือ custom Equal() method


4. Methods — Function ผูกกับ Type

Method คือฟังก์ชันที่ผูกกับ type หนึ่ง — เขียน "receiver" ไว้ในวงเล็บหน้าชื่อ method (func (r Rectangle) Area()) receiver ทำหน้าที่เหมือน this/self ในภาษาอื่น ทำให้เรียกแบบ r.Area() ได้และจัดกลุ่มพฤติกรรมเข้ากับข้อมูล:

go
type Rectangle struct {
    Width, Height float64
}

// Method
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

// Call
r := Rectangle{Width: 3, Height: 4}
r.Area()                       // 12
r.Perimeter()                  // 14
text
ส่วนประกอบ:
func (receiver Type) MethodName(params) returnType { ... }

                                          receiver = "this" / "self"

5. Value Receiver vs Pointer Receiver

⚠️ C# ไม่มีแนวคิดนี้ — method บน C# class แก้ field ได้เสมอ (this เป็น reference) Go ต้องเลือกเอง: (r *T) ถ้าอยากแก้ค่าตัวจริง value receiver = ทำงานบนสำเนา แก้เท่าไหร่ก็ไม่ติดต้นฉบับ (คล้าย mutating method บน C# struct ที่ต้องระวังเหมือนกัน)

Value Receiver — Copy

go
func (r Rectangle) Scale(factor float64) {
    r.Width *= factor          // modifies copy, not original
    r.Height *= factor
}

r := Rectangle{Width: 3, Height: 4}
r.Scale(2)
fmt.Println(r.Width)           // 3 (unchanged!)

Pointer Receiver — Modify Original

go
func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor          // modifies original
    r.Height *= factor
}

r := Rectangle{Width: 3, Height: 4}
r.Scale(2)                     // Go auto: (&r).Scale(2)
fmt.Println(r.Width)           // 6 ✅

กฎ — เลือกอันไหน

text
Pointer receiver เมื่อ:
✅ Method modify struct
✅ Struct ใหญ่ (copy แพง)
✅ Consistency — ถ้า method อื่นใช้ pointer แล้ว ทั้งหมดควรใช้ pointer
✅ Contains `sync.Mutex` (ตัวล็อกกันการแก้ค่าพร้อมกัน — ถ้าก๊อปจะแยกล็อกออกเป็น 2 ตัว ทำให้ระบบล็อกพัง — บท 8 อธิบายเต็ม) or other "must not copy" type (ยังไม่ต้องเข้าใจกลไกเบื้องหลังตอนนี้ — จำแค่กฎ: มี Mutex → ใช้ pointer receiver)

Value receiver เมื่อ:
✅ Struct เล็ก (int wrap, small struct)
✅ Immutable operation (Area, ToString)
✅ Primitive-like type เล็ก ๆ ไม่ mutate (เช่น Celsius float64 ด้านล่าง)

Convention

text
เลือกตาม mutation/ขนาด/consistency — ถ้ายังไม่แน่ใจ ให้ "when in doubt, use pointer receiver"
(ไม่ใช่ว่าทุกโปรเจกต์ default ใช้ pointer เสมอ — type เล็กที่ไม่ mutate เช่น Celsius, Duration ก็ยังใช้ value receiver ปกติ)
go
// ❌ Mixed (confusing)
func (r Rectangle) Area() float64 { ... }
func (r *Rectangle) Scale(...)  { ... }

// ✅ Consistent
func (r *Rectangle) Area() float64 { ... }
func (r *Rectangle) Scale(...)  { ... }

→ stylistic decision — Go community ผสมกัน — แต่ consistency สำคัญ


6. Method on Any Type

ไม่ใช่แค่ struct — ใส่ method ได้บน type ใด ๆ ที่ defined ใน package เดียวกัน:

go
type Celsius float64

func (c Celsius) Fahrenheit() float64 {
    return float64(c)*9/5 + 32
}

c := Celsius(100)
c.Fahrenheit()                 // 212
go
type IntSlice []int

func (s IntSlice) Sum() int {
    total := 0
    for _, n := range s {
        total += n
    }
    return total
}

nums := IntSlice{1, 2, 3}
nums.Sum()                     // 6

กฎ

  • ✅ Method บน type ที่ define ใน package เดียวกัน
  • ❌ Method บน built-in type ตรง ๆ (int, string) — ต้อง wrap
  • ❌ Method บน type จาก package อื่น
go
// ❌
func (i int) Double() int { ... }    // compile error

// ✅ Wrap first
type MyInt int
func (i MyInt) Double() MyInt { ... }

7. Constructor Pattern

Go ไม่มี constructor ในตัวแบบภาษา OOP — แต่มีธรรมเนียม (convention) ให้เขียนฟังก์ชันชื่อขึ้นต้นด้วย New* ทำหน้าที่สร้างและตรวจสอบความถูกต้องของ struct ก่อนคืนค่า (มักคืน pointer + error) ทำให้มั่นใจว่าทุก object ที่สร้างมาอยู่ในสถานะที่ถูกต้อง:

⚠️ สำหรับ dev .NET: Go ไม่มี constructor keyword เหมือน C# (public User(string name)) — ใช้ธรรมเนียม func NewUser(...) *User แทน และ new(User) ใน Go ไม่เหมือน new User() ของ C# (Go new แค่คืน pointer ที่ทุก field เป็น zero value ไม่รับ argument ไม่เรียก logic ใด ๆ)

go
type User struct {
    Name  string
    Email string
    Age   int
}

// Constructor
func NewUser(name, email string) (*User, error) {
    if name == "" {
        return nil, errors.New("name required")
    }
    if !strings.Contains(email, "@") {
        return nil, errors.New("invalid email")
    }
    return &User{
        Name:  name,
        Email: email,
        Age:   0,             // default
    }, nil
}

// Usage
user, err := NewUser("Anna", "anna@x.com")
if err != nil {
    log.Fatal(err)
}

Multiple Constructors

ตัวอย่างด้านล่างเป็น signatures ทางเลือก — ไม่ใช่ทั้งหมดในไฟล์เดียวกัน (ชื่อ NewUser ซ้ำไม่ได้ใน Go) เลือก pattern ที่เหมาะกับงานของคุณ:

go
// ตัวเลือก 1 — signature ง่าย (ไม่ return error)
func NewUser(name, email string) *User { ... }

// ตัวเลือก 2 — เพิ่ม parameter
func NewUserWithAge(name, email string, age int) *User { ... }

// ตัวเลือก 3 — สร้าง admin user โดยเฉพาะ
func NewAdminUser(name, email string) *User { ... }

หรือใช้ Functional Options (บทที่ 3):

go
func NewUser(name, email string, opts ...Option) *User {
    u := &User{Name: name, Email: email}
    for _, opt := range opts {
        opt(u)
    }
    return u
}

// Usage
u := NewUser("Anna", "anna@x.com",
    WithAge(25),
    WithRole("admin"),
)

8. Struct Tag

💡 reflection (รีเฟล็กชัน) = ความสามารถของโปรแกรมในการสำรวจชนิด/โครงสร้างของตัวเองตอนรัน

คิดว่า struct tag เป็นเหมือน "ป้ายชื่อ" ที่ติดไว้บนกล่อง เวลา library ส่งข้อมูล (Marshal) มันอ่านป้ายเหล่านี้เพื่อรู้ว่าจะใช้ชื่ออะไร คุณไม่ต้องรู้ว่ามันอ่านยังไง — รู้แค่รูปแบบ: json:"fieldname" ก็พอ

Struct tag คือข้อความ metadata (ข้อมูลกำกับ field) ที่แปะไว้ท้าย field ใน backtick — library ต่าง ๆ อ่านผ่าน reflection เพื่อรู้ว่าจะจัดการ field นั้นอย่างไร

ที่พบบ่อยสุด:

  • json:"..." บอกชื่อ field ตอนแปลงเป็น JSON
  • db:"..." map field ไปคอลัมน์ใน DB
  • validate:"..." กฎตรวจสอบความถูกต้อง

⚠️ สำหรับ dev .NET: struct tag ≈ C# attribute — json:"id"[JsonPropertyName("id")], json:"-"[JsonIgnore], validate:"..."[Required] ฯลฯ แต่ Go tag เป็น string ดิบที่อ่านผ่าน reflection ตอน runtime ไม่ใช่ type-safe attribute แบบ C#

go
type User struct {
    ID    int    `json:"id" db:"user_id"`
    Name  string `json:"name" db:"user_name" validate:"required,min=2"`
    Email string `json:"email,omitempty" db:"email"`
}

Tag = metadata — library ใช้ผ่าน reflection

Common Tags

text
json:    encoding/json
xml:     encoding/xml
yaml:    yaml.v3
db:      sqlx, gorm
gorm:    GORM ORM
validate: validator library

JSON Example

Marshal = การ marshalling/serialize (แปลงโครงสร้างข้อมูลในโปรแกรมให้เป็นข้อความ/ไบต์เพื่อส่งหรือเก็บ เช่น struct → JSON), Unmarshal = ทำย้อนกลับ (JSON → struct)

go
import "encoding/json"

u := User{ID: 1, Name: "Anna", Email: "anna@x.com"}

data, err := json.Marshal(u)       // serialize: struct → JSON
// ในงานจริงต้องเช็ค err เสมอ
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(data))
// {"id":1,"name":"Anna","email":"anna@x.com"}
go
// Omitempty — ตัวอย่างแยก (variable ใหม่ uNoEmail, dataOmit)
uNoEmail := User{ID: 1, Name: "Anna"}    // no email
dataOmit, err := json.Marshal(uNoEmail)
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(dataOmit))
// {"id":1,"name":"Anna"}  ← email omitted
go
// Unmarshal — ตัวอย่างแยก
var u2 User
if err := json.Unmarshal([]byte(`{"id":1,"name":"Anna"}`), &u2); err != nil {
    log.Fatal(err)
}

Skip Field

go
type User struct {
    Password string `json:"-"`       // never serialize
}

9. Embedding (Composition) — ⭐ Go's "Inheritance"

Go ไม่มี class inheritance — ใช้ embedding แทน

⚠️ FALSE FRIEND สำหรับ dev .NET: ต่างจาก C# inheritance (: BaseClass) — embedding ไม่มีความสัมพันธ์แบบ is-a จริง Dog ไม่ใช่ Animal (ส่งเป็น Animal ตรง ๆ ไม่ได้), "override" ด้านล่างก็ไม่ใช่ virtual method จริง (ไม่มี dynamic dispatch ผ่าน base type) — polymorphism ใน Go ทำผ่าน interface เท่านั้น ไม่ใช่ผ่าน base class

go
type Animal struct {
    Name string
    Age  int
}

func (a Animal) Greet() string {
    return "Hi, I'm " + a.Name
}

type Dog struct {
    Animal              // embed (no field name!)
    Breed   string
}

dog := Dog{
    Animal: Animal{Name: "Rex", Age: 3},
    Breed:  "Labrador",
}

// Access embedded field directly
dog.Name                           // "Rex" (promoted)
dog.Age                            // 3
dog.Breed                          // "Labrador"
dog.Animal.Name                    // also works (explicit)

// Call embedded method
dog.Greet()                        // "Hi, I'm Rex" (promoted)

Pros vs Inheritance

text
✅ Composition over inheritance (ประกอบจากชิ้นเล็ก ดีกว่าสืบทอด)
   — หลักการของ OOD (Object-Oriented Design = การออกแบบเชิงวัตถุ)
   — ต่างจาก OOP (Object-Oriented Programming = การเขียนโปรแกรมเชิงวัตถุ):
     OOP = วิธีเขียนโค้ด (มี class, object)
     OOD = วิธีออกแบบโครงสร้าง (ควรประกอบหรือสืบทอด?)
✅ No diamond problem (ปัญหาที่เกิดตอนสืบทอดจาก 2 class แม่ที่มี method ชื่อเดียวกัน แล้วไม่รู้จะเรียกอันไหน — พบใน C++/บาง OOP ที่รองรับ multiple inheritance)
✅ Explicit — you see what's embedded (ชัดเจน — เห็นได้ว่าฝังอะไรไว้)
✅ Easy to swap implementation (เปลี่ยน implementation ภายในได้ง่าย)

Override Method

go
type Dog struct {
    Animal
    Breed string
}

// Dog has its own Greet — "overrides" Animal's
func (d Dog) Greet() string {
    return "Woof! I'm " + d.Name
}

dog.Greet()                        // "Woof! I'm Rex"
dog.Animal.Greet()                 // "Hi, I'm Rex" (parent)

Multiple Embed

go
type Logger struct{}
func (l Logger) Log(msg string) {}             // stub — implementation ไว้ใส่จริง

type Cache struct{}
func (c Cache) Get(key string) any { return nil }   // any = interface{} ตั้งแต่ Go 1.18

type Service struct {
    Logger
    Cache
    Name string
}

s := Service{}
s.Log("hello")           // from Logger
s.Get("key")             // from Cache

⚠️ ถ้า method ชื่อชน → ต้อง access ผ่าน explicit s.Logger.Log()


10. Anonymous Struct

บางครั้งเราต้องการ struct ใช้ครั้งเดียวโดยไม่อยากตั้งชื่อ type — เขียน struct แบบไม่มีชื่อ (anonymous struct) ตรงนั้นเลยได้.

Use case ที่เจอบ่อย คือใช้ประกอบ response/request DTO (Data Transfer Object — struct ที่ใช้ส่งข้อมูลข้ามชั้น เช่น ระหว่าง HTTP handler กับ client) สั้น ๆ ภายในฟังก์ชันเดียว:

go
// One-off struct
config := struct {
    Host string
    Port int
}{
    Host: "localhost",
    Port: 8080,
}

fmt.Println(config.Host)

// Useful for response/request DTOs
func handler(w http.ResponseWriter, r *http.Request) {
    response := struct {
        Status string `json:"status"`
        Count  int    `json:"count"`
    }{
        Status: "ok",
        Count:  42,
    }
    json.NewEncoder(w).Encode(response)
}

11. Method Set

go
type Counter struct {
    count int
}

func (c Counter) Get() int { return c.count }
func (c *Counter) Inc()    { c.count++ }

Method set:

  • Counter: Get() only (value receiver)
  • *Counter: Get() + Inc() (both)
go
c := Counter{}
c.Inc()        // Go auto: (&c).Inc()  → OK ถ้า c addressable
c.Get()        // OK

pc := &Counter{}
pc.Inc()       // OK
pc.Get()       // Go auto: (*pc).Get()  → OK

⚠️ ใช้ไม่ได้ถ้าค่านั้น "ไม่ addressable" เช่น element ใน map:

go
m := map[int]Counter{}
m[0].Inc()     // ❌ compile error: cannot call pointer method on map element (not addressable)

→ ใน practice — ไม่ต้องคิดมาก Go ช่วย convert (ยกเว้นกรณี non-addressable แบบนี้)

⚠️ Interface satisfaction (การที่ type "เข้าเงื่อนไข" ของ interface — Go ตรวจจากชุด method ที่มี ไม่ต้องประกาศว่า implements เหมือน Java) ใช้ method set ตรง ๆ — รายละเอียดเต็มบทที่ 6

สำหรับ dev .NET: นี่คือความต่างใหญ่ที่สุดของ interface Go vs C# — C# เป็น nominal typing (ต้องประกาศ class User : IShape ชัดเจน) แต่ Go เป็น structural typing: แค่ type มี method ครบตามที่ interface ต้องการ ก็ satisfy อัตโนมัติโดยไม่ต้องประกาศอะไรเลย


12. Stringer Interface — เวทมนตร์ของ fmt.Println

Stringer คืออินเทอร์เฟซมาตรฐานของ Go สำหรับ "การแปลงเป็น string" — ถ้า type ของเรามี method String() string เมื่อไหร่ fmt.Println (และ fmt.Sprintf %v ฯลฯ) จะเรียก method นี้ให้อัตโนมัติ แทนการพิมพ์ค่าดิบ. ที่เรียกว่า "magic / เวทมนตร์" คือเราไม่ต้องบอก fmt ว่า type เราเป็น Stringer — แค่มี String() ก็พอ.

เหมาะมากเวลาทำ enum ให้แสดงชื่อแทนตัวเลข

สำหรับ dev .NET: String() method ≈ C# override ToString()fmt.Println เรียกอัตโนมัติเหมือน Console.WriteLine เรียก ToString() ต่างกันตรงที่ Go เป็น structural (แค่มี String() string ก็พอ ไม่ต้อง override หรือประกาศอะไรเพิ่ม):

go
type Color int

const (
    Red Color = iota
    Green
    Blue
)

// Implement Stringer interface
func (c Color) String() string {
    switch c {
    case Red:   return "red"
    case Green: return "green"
    case Blue:  return "blue"
    default:    return "unknown"
    }
}

fmt.Println(Red)                  // prints "red" (not 0)
fmt.Println("Color:", Green)       // "Color: green"

fmt.Println check ว่า value มี String() string → call → print result


13. ตัวอย่างเต็ม — Bank Account

มาประกอบทุกอย่างในบทเข้าด้วยกันด้วยตัวอย่างบัญชีธนาคาร — ใช้ struct เก็บข้อมูล, constructor NewAccount, และ pointer receiver เพื่อแก้ยอดเงินได้จริง.

ตัวอย่างนี้มี 2 ส่วน:

  • ส่วนหลัก (อ่านทุกคน): struct + constructor + pointer receiver + error return — ดูได้เลยโดยไม่ต้องรู้ Mutex
  • ส่วนเสริม (sync.Mutex): ป้องกันการแก้ยอดพร้อมกันจากหลาย goroutine — ข้ามได้ถ้ายังไม่ถึงบท 8

💡 ถ้าอ่านแล้วงงเรื่อง Mutex: มองข้าม mu.Lock() / mu.Unlock() ไปก่อน — ตรรกะหลักของ struct และ method ยังตามได้ครบ sync.Mutex = "กลอนล็อก" กันคนแก้ยอดเงินชนกันพร้อมกัน — บท 08 Concurrency อธิบายเต็ม

go
package main

import (
    "errors"
    "fmt"
    "sync"
)

// Account must not be copied — มี sync.Mutex อยู่ภายใน
// (ถ้าก๊อป struct นี้ mutex จะแยกเป็น 2 ตัวแยกกัน ทำให้ lock พัง — ดูบท 8)
// ใช้ผ่าน *Account เท่านั้น (constructor คืน *Account ให้แล้ว)
type Account struct {
    mu      sync.Mutex
    holder  string
    balance float64
}

func NewAccount(holder string, initial float64) *Account {
    return &Account{
        holder:  holder,
        balance: initial,
    }
}

func (a *Account) Deposit(amount float64) error {
    if amount <= 0 {
        return errors.New("amount must be positive")
    }
    a.mu.Lock()
    defer a.mu.Unlock()
    a.balance += amount
    return nil
}

func (a *Account) Withdraw(amount float64) error {
    if amount <= 0 {
        return errors.New("amount must be positive")
    }
    a.mu.Lock()
    defer a.mu.Unlock()
    if a.balance < amount {
        return errors.New("insufficient funds")
    }
    a.balance -= amount
    return nil
}

func (a *Account) Balance() float64 {
    a.mu.Lock()
    defer a.mu.Unlock()
    return a.balance
}

// (ข้ามคอมเมนต์นี้ได้ถ้ายังไม่ถึงบท 8 — เกี่ยวกับ Mutex เท่านั้น)
// คำเตือน: ห้ามเรียก fmt.Println(acc) จากข้างในเมธอดอื่นที่ถือ lock ค้างอยู่
// ไม่งั้นโปรแกรมจะค้าง (deadlock) — รายละเอียดเรื่อง Mutex ดูบท 8
func (a *Account) String() string {
    return fmt.Sprintf("Account{holder=%s, balance=$%.2f}", a.holder, a.Balance())
}

func main() {
    acc := NewAccount("Anna", 1000)
    
    acc.Deposit(500)
    acc.Withdraw(200)
    
    fmt.Println(acc)
    // Account{holder=Anna, balance=$1300.00}
    
    if err := acc.Withdraw(5000); err != nil {
        fmt.Println("Error:", err)
        // Error: insufficient funds
    }
}

ใช้:

  • Pointer receiver (modify balance)
  • Constructor NewAccount
  • Mutex for concurrency
  • String() method for nice print
  • Error return for validation

14. Embedding Interface (preview — บทที่ 6)

⚠️ ส่วนนี้แตะเรื่อง interface ที่ยังไม่ได้สอนเต็ม — ตอนนี้แค่ดูให้รู้ว่า "embedding ใช้กับ interface ได้เหมือนใช้กับ struct" ก็พอ. รายละเอียดเต็มอยู่บทที่ 6

เราฝัง (embed) interface เล็ก ๆ เข้าด้วยกันเพื่อสร้าง interface ใหญ่ขึ้นได้ — เช่น ReadCloser = Reader + Closer. วิธีนี้คือหัวใจของการออกแบบ interface แบบ Go ที่นิยมประกอบจากชิ้นเล็ก:

go
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

// Embed interfaces
type ReadCloser interface {
    Reader
    Closer
}

// = same as
type ReadCloser interface {
    Read(p []byte) (n int, err error)
    Close() error
}

→ ดูบทที่ 6 (Interfaces)


15. Visibility

ทบทวนกฎ visibility ในบริบทของ struct: field/method/function ที่ขึ้นต้นด้วยตัวพิมพ์ใหญ่ = public (package อื่นเข้าถึงได้), พิมพ์เล็ก = private (ใช้ได้แค่ใน package เดียวกัน) — นี่คือวิธีซ่อนข้อมูลภายใน (encapsulation) ของ Go:

go
package mypackage

// Public (exported)
type User struct {
    Name  string    // public field
    age   int       // private field (lowercase)
}

// Public method
func (u *User) GetAge() int {
    return u.age
}

// Private method
func (u *User) calculateBonus() float64 {
    return float64(u.age) * 0.01
}

// Private function
func validate(u *User) error {
    return nil
}
go
// other_package.go
import "mypackage"

u := mypackage.User{Name: "Anna"}    // ✅ Name accessible
u.age = 25                             // ❌ compile error — age is private
u.GetAge()                             // ✅
u.calculateBonus()                     // ❌ compile error

16. Common Patterns

รวมแพตเทิร์นการออกแบบที่ใช้ struct + method ที่พบบ่อยในโค้ด Go จริง — Builder (ต่อ method เป็นทอด ๆ เพื่อตั้งค่า) และ Repository (แยกชั้นเข้าถึงข้อมูลออกจาก business logic):

Builder Pattern (Functional Options preferred)

go
type ServerBuilder struct {
    addr    string
    timeout time.Duration
}

func NewServerBuilder() *ServerBuilder {
    return &ServerBuilder{addr: ":8080", timeout: 30 * time.Second}
}

func (b *ServerBuilder) Addr(addr string) *ServerBuilder {
    b.addr = addr
    return b
}

func (b *ServerBuilder) Timeout(d time.Duration) *ServerBuilder {
    b.timeout = d
    return b
}

func (b *ServerBuilder) Build() *Server {
    return &Server{addr: b.addr, timeout: b.timeout}
}

// Usage
s := NewServerBuilder().Addr(":9000").Timeout(60*time.Second).Build()

Go community prefer Functional Options (บทที่ 3) — แต่ Builder OK เหมือนกัน

Repository Pattern

แนวคิดของ Repository คือ แยกชั้นเข้าถึงข้อมูล ออกจาก business logic — Service เรียก Repository ผ่าน method แทนการเขียน SQL ตรง ๆ. รุ่นง่ายสุดเก็บข้อมูลใน map ไว้ก่อนได้:

go
type User struct {
    ID   int
    Name string
}

// In-memory repository — เก็บใน map (เหมาะกับการทดสอบ/ตัวอย่าง)
type UserRepository struct {
    users map[int]*User
}

func NewUserRepository() *UserRepository {
    return &UserRepository{users: map[int]*User{}}
}

func (r *UserRepository) FindByID(id int) (*User, error) {
    u, ok := r.users[id]
    if !ok {
        return nil, errors.New("user not found")
    }
    return u, nil
}

func (r *UserRepository) Create(u *User) error {
    r.users[u.ID] = u
    return nil
}

💡 ของจริงในงาน production มักใช้ *sql.DB แทน map — interface ภายนอกยังเหมือนเดิม (FindByID, Create) แค่ implementation ภายในใช้ r.db.QueryRow(...).Scan(...) แทน. รายละเอียด database/sql เต็ม ๆ อยู่บท 14 Database — ตอนนี้ดูแค่ "รูปแบบการแยกชั้น" ก็พอ

Service Pattern

go
type UserService struct {
    repo   *UserRepository
    logger *log.Logger
}

func NewUserService(repo *UserRepository, logger *log.Logger) *UserService {
    return &UserService{repo: repo, logger: logger}
}

func (s *UserService) GetUser(id int) (*User, error) {
    s.logger.Printf("Get user %d", id)
    return s.repo.FindByID(id)
}

17. ⚠️ Common Pitfalls

ตารางนี้รวมข้อผิดพลาดเรื่อง struct และ method ที่มือใหม่มักเจอ คู่กับวิธีที่ถูก — โดยเฉพาะการผสม value/pointer receiver ไม่สม่ำเสมอ, ก๊อป struct ที่มี mutex (ทำให้ lock พัง) และลืมใช้ pointer receiver เวลาต้องแก้ค่า:

Mix value + pointer receiverconsistent — เลือก 1
Forget pointer for "modify method"use pointer receiver
Copy struct with mutexmutex must be pointer-receiver
Embed pointer ที่ nilinitialize before use
Public field for everythingencapsulate — private + method
Forget struct tag for JSONadd json:"..."
Method on big struct valueuse pointer for performance
Compare struct with sliceimplement Equal() method

18. Method vs Function

go
// Method
func (u *User) Save() error {
    return repo.save(u)
}

// Or function
func Save(u *User) error {
    return repo.save(u)
}

เลือกอันไหน?

Method เมื่อ:

  • Behavior ผูกกับ data
  • Implement interface
  • Encapsulate state

Function เมื่อ:

  • Pure function (no state)
  • Operate ข้าม type หลายตัว
  • Utility / helper

เมื่อโปรเจกต์โตขึ้น การจัดวางไฟล์ให้เป็นระเบียบช่วยให้ทีมหาของเจอและแยกความรับผิดชอบชัด โครงสร้างด้านล่างเป็นแบบที่ชุมชน Go นิยม (แยก internal/ สำหรับโค้ดส่วนตัว, จัดกลุ่มตาม domain เช่น user):

text
myapp/
├── go.mod
├── main.go
├── internal/
│   ├── user/
│   │   ├── user.go            # struct
│   │   ├── repository.go      # data access
│   │   ├── service.go         # business logic
│   │   └── handler.go         # HTTP
│   └── order/
│       ├── order.go
│       ├── repository.go
│       └── ...
├── pkg/
│   └── logger/
└── cmd/
    └── server/
        └── main.go

→ 1 package per concept, struct + method + private helper รวมกัน


20. Checkpoint

🛠️ Checkpoint 5.1 — Shape Library (ไลบรารีรูปทรง)
สร้าง:

  • Rectangle{Width, Height} — Area (พื้นที่), Perimeter (เส้นรอบรูป), Scale (ย่อ-ขยาย)
  • Circle{Radius} — Area, Perimeter, Scale
  • Triangle{A, B, C} — Area (สูตรของเฮรอน / Heron's formula)
  • ทดสอบทุก method

🛠️ Checkpoint 5.2 — Counter with Mutex (ตัวนับที่ปลอดภัยต่อหลาย goroutine)
(ใช้ goroutine/mutex ของบท 08 — ถ้ายังไม่เรียน ข้ามไปก่อนได้) ทำ Counter ที่:

  • Inc(), Dec(), Get(), Reset()
  • Thread-safe (ปลอดภัยเมื่อถูกเรียกพร้อมกันหลายงาน)
  • ทดสอบเรียกพร้อมกัน (concurrent) ด้วย goroutine หลายตัว

🛠️ Checkpoint 5.3 — Bank Account (บัญชีธนาคาร)
ทำตัวอย่างข้อ 13 — ขยาย:

  • โอนเงินระหว่างบัญชี (atomic = สำเร็จทั้งคู่หรือไม่สำเร็จทั้งคู่)
  • บันทึกประวัติธุรกรรม (transaction log)
  • สรุปรายการ (statement) N รายการล่าสุด

🛠️ Checkpoint 5.4 — Embedding (การฝัง struct)

  • Animal (มี Name, method Speak)
  • Dog ฝัง Animal แล้ว override (เขียนทับ) Speak
  • ServiceDog ฝัง Dog แล้วเพิ่ม method Train
  • ทดสอบ polymorphism (การที่ type ต่างกันถูกเรียกผ่าน interface เดียวกัน) ผ่าน interface (ส่วน polymorphism ผ่าน interface — กลับมาทำหลังเรียนบท 6 ก็ได้)

🛠️ Checkpoint 5.5 — JSON Tag
สร้าง struct User, Order, Product:

  • field เป็น PascalCase ใน Go
  • ผลลัพธ์ JSON เป็น snake_case (ชื่อแบบ user_name)
  • ใส่ omitempty บางตัว (ตัดทิ้งถ้าเป็นค่าว่าง)
  • ใส่ private field ที่จะไม่ถูก serialize (แปลงออกเป็น JSON)

21. สรุปบท

Struct = group ของ field — initialize ด้วย Type{field: value}
✅ Struct ทุกตัวมี zero value (no constructor needed)
&User{} หรือ new(User) สร้าง pointer
Method: func (r Type) Method() { ... }
Pointer receiver เมื่อ modify หรือ struct ใหญ่ — consistent
✅ Method on any type ที่ defined ใน package เดียวกัน (ไม่ใช่แค่ struct)
Constructor pattern: NewXxx() function — Go ไม่มี constructor keyword
Struct tag — metadata สำหรับ JSON / DB / validation
Embedding = "composition over inheritance" — promote field + method
Visibility ผูกกับ capitalization — PascalCase = public
String() method — fmt.Println auto-call
✅ Project structure: domain package + struct + repository + service


← บทที่ 4 | บทที่ 6 → Interfaces