โหมดมืด
บทที่ 5 — Structs และ Methods
หลังจบบท คุณจะ:
- สร้าง 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-dereferenceZero 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 literalgo
// 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() // 14text
ส่วนประกอบ:
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() // 212go
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 ตอนแปลงเป็น JSONdb:"..."map field ไปคอลัมน์ใน DBvalidate:"..."กฎตรวจสอบความถูกต้อง
⚠️ สำหรับ 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 libraryJSON 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 omittedgo
// 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 error16. 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 receiver | consistent — เลือก 1 |
| Forget pointer for "modify method" | use pointer receiver |
| Copy struct with mutex | mutex must be pointer-receiver |
| Embed pointer ที่ nil | initialize before use |
| Public field for everything | encapsulate — private + method |
| Forget struct tag for JSON | add json:"..." |
| Method on big struct value | use pointer for performance |
| Compare struct with slice | implement 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
19. Recommended Project Structure
เมื่อโปรเจกต์โตขึ้น การจัดวางไฟล์ให้เป็นระเบียบช่วยให้ทีมหาของเจอและแยกความรับผิดชอบชัด โครงสร้างด้านล่างเป็นแบบที่ชุมชน 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, ScaleTriangle{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 (เขียนทับ) SpeakServiceDogฝัง 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