สารบัญ
- Underlying Type & Alias
- Interface & Duck Typing
- any, Assertion, Type Switch
- Standard Interfaces
- Error Architecture
- JSON & Struct Tags
- Encoder / Decoder
- ตารางกับดักรวม
1 · Underlying Type & Alias
type Score float64 // defined type — type ใหม่ ไม่เท่ากับ float64
type Price = float64 // alias — คือ float64 ตัวเดียวกันเป๊ะ
| type T U | type T = U |
| type ใหม่ | ✅ | ❌ |
assign ตรง ๆ กับ U | ❌ ต้อง U(v) | ✅ |
| แปะ method | ✅ | ❌ cannot define new methods on non-local type |
%T | main.T | U |
- underlying type เดียวกัน ≠ type เดียวกัน —
Score กับ Price ที่ต่างมาจาก float64
เอามา == กันไม่ได้
- alias ที่มีอยู่แล้วในภาษา:
any=interface{} ·
byte=uint8 · rune=int32
- ใช้ defined type เมื่ออยากให้ compiler แยกความหมาย
(
UserID/ProductID, Baht/Satang)
type Score float64
func (s Score) Passed() bool { return s >= 50 } // method บน type ที่ไม่ใช่ struct ก็ได้
a := Score(80.5) + Score(10) // 90.5 · %T = main.Score (ไม่ใช่ float64)
2 · Interface & Duck Typing
type Notifier interface {
Notify(message string) error
}
type EmailNotifier struct{}
func (EmailNotifier) Notify(message string) error { return nil }
// ไม่ต้องเขียน implements — มี method ครบก็ใช้ได้เลย (structural typing)
Method set — กฎที่ทำให้ compile ไม่ผ่านบ่อยที่สุด
| receiver | T เป็น interface ได้ | *T เป็น interface ได้ |
func (t T) M() | ✅ | ✅ |
func (t *T) M() | ❌ | ✅ |
// error ที่จะเจอ:
// T does not implement I (method M has pointer receiver)
// ทางแก้: ใส่ & → var i I = &T{}
Dependency injection ด้วย interface
type PaymentGateway interface { Pay(amount int64) error }
type CheckoutService struct { gateway PaymentGateway }
func NewCheckoutService(gateway PaymentGateway) *CheckoutService {
return &CheckoutService{gateway: gateway}
}
// สลับ PaypalGateway{} / GooglePayGateway{} / FakeGateway{} ได้ที่จุดเดียว
Interface embedding
type Storage interface {
Saver // รวม method ของ Saver มา ไม่ใช่ field
Loader
Deleter
}
// ของจริง: io.ReadWriter = io.Reader + io.Writer
กฎออกแบบ
- สร้าง interface ตามสิ่งที่ผู้เรียกต้องใช้ ไม่ใช่ตามทุกอย่างที่ type ทำได้
- Accept interfaces, return structs — พารามิเตอร์เป็น interface ค่าคืนเป็น struct
- ประกาศ interface ไว้ใน package ของฝั่งที่ใช้ ไม่ใช่ฝั่งที่ implement
3 · any, Type Assertion, Type Switch
type any = interface{} // alias · interface ที่ไม่ขอ method อะไรเลย
ตัวแปร interface เก็บ สองช่องเสมอ:
┌──────────┬──────────┐
│ type │ value │ %T อ่านช่องซ้าย · %v อ่านช่องขวา
└──────────┴──────────┘
interface == nil ก็ต่อเมื่อ ทั้งสองช่องว่าง
| เขียน | ถ้า type ไม่ตรง | ใช้เมื่อ |
s := i.(string) | 💥 panic | มั่นใจ 100% เท่านั้น |
s, ok := i.(string) | s = zero value, ok = false | ค่าเริ่มต้น ใช้อันนี้ |
switch v := i.(type) | ตกลง default | เช็คหลาย type |
switch value := v.(type) { // .(type) เขียนได้เฉพาะใน switch
case string: // value เป็น string จริง ๆ ในบล็อกนี้ ไม่ต้อง assert ซ้ำ
case int:
case nil: // เช็ค nil interface ได้
case fmt.Stringer, error: // หลาย type ใน case เดียว → value กลับเป็น any
default:
}
- Go เลือก case แรกที่ตรง ไม่ใช่ที่ตรงที่สุด —
type ที่ implement ทั้ง
error และ fmt.Stringer
จะเข้า case ไหนขึ้นกับลำดับที่คุณเขียน
panic ที่จะเห็น:
interface conversion: interface {} is string, not int
4 · Standard Interfaces ที่ควรรู้จัก
| Interface | Method | ได้อะไร |
fmt.Stringer | String() string | fmt.Println พิมพ์ตามที่คุณกำหนด |
error | Error() string | เป็น error ได้ |
io.Reader | Read([]byte) (int, error) | เสียบเข้ากับ ecosystem ทั้งหมดของ Go |
io.Writer | Write([]byte) (int, error) | เขียนลงไฟล์/network/buffer ได้เหมือนกันหมด |
json.Marshaler | MarshalJSON() ([]byte, error) | คุมรูปแบบ JSON เอง |
func (p Person) String() string { return p.Name + " " + p.Surname }
fmt.Println(p) // มี String() → Alice Wonder
// ไม่มี → {Alice Wonder}
- อย่าเขียน
fmt.Sprintf("%v", p) ข้างใน String()
→ วนไม่รู้จบ stack overflow · อ้าง field ตรง ๆ แทน
- type ที่มีทั้ง
Error() และ String() →
fmt เลือก Error() ก่อน
5 · Error Architecture
type error interface { Error() string } // built-in · แค่นี้จริง ๆ
สร้าง error
| ใช้เมื่อ | ห่อได้ | == ได้ |
errors.New("...") | ข้อความคงที่ / sentinel | ❌ | ✅ |
fmt.Errorf("...%w", err) | เติมบริบท / ห่อ error เดิม | ✅ | ❌ |
&MyError{...} | ต้องพก field ไปด้วย | ✅ (ถ้ามี Unwrap) | ❌ |
Sentinel + Error chain
var ErrUserNotFound = errors.New("user not found") // ระดับ package · ขึ้นต้น Err
// repository: fmt.Errorf("query user: %w", err)
// service: fmt.Errorf("get profile %s: %w", id, err)
// ได้: get profile U-101: query user: user not found
%v ทำให้โซ่ขาด แต่ข้อความเหมือนเดิมทุกตัวอักษร —
errors.Is จะคืน false เงียบ ๆ
ใช้ %w เท่านั้นเวลาห่อ error
ตรวจ error
| errors.Is(err, target) | errors.As(err, &target) |
| ถาม | "มี error ตัวนี้ ในโซ่ไหม" | "มี error ชนิดนี้ ในโซ่ไหม" |
| คู่กับ | sentinel | custom struct |
| ได้กลับ | bool | bool + ตัวจริงพร้อม field |
ต้องมี & | ❌ | ✅ บังคับ |
if errors.Is(err, ErrUserNotFound) { /* ตอบ 404 */ }
var appErr *AppError
if errors.As(err, &appErr) {
http.Error(w, appErr.Message, appErr.HTTPStatus)
}
Custom error struct — โครงที่ใช้จริง
type AppError struct {
Op string // เกิดที่ไหน — "UserService.GetUser"
Code string // รหัสให้เครื่องอ่าน — "USER_NOT_FOUND"
HTTPStatus int // ตอบ client ยังไง
Message string // ข้อความให้ผู้ใช้อ่าน
Err error // error เดิม
}
func (e *AppError) Error() string { ... }
func (e *AppError) Unwrap() error { return e.Err } // ⚠️ ขาดไม่ได้
- ไม่มี
Unwrap() → errors.Is พัง
แต่ errors.As ยังหา type ชั้นนอกสุดเจอ
- ห้ามตรวจ error ด้วย
err.Error() == "..."
typed nil — คืน var e *AppError = nil
จากฟังก์ชันที่ประกาศคืน error จะทำให้
err != nil เป็น true
เพราะช่อง type ไม่ว่าง · ให้เขียน return nil ตรง ๆ
6 · JSON & Struct Tags
json.Marshal(v) // Go → []byte (ส่งออก)
json.Unmarshal(data, &v) // []byte → Go (รับเข้า) · ลืม & = error
json.MarshalIndent(v, "", " ") // สวยงามสำหรับ log/debug
Tag options
| Tag | ผล |
json:"userId" | เปลี่ยนชื่อ key |
json:"nickname,omitempty" | ตัดทิ้งถ้าเป็น zero value |
json:"-" | ไม่เข้าไม่ออก JSON เลย |
json:"-," | key ชื่อ "-" จริง ๆ (สังเกต comma) |
json:",string" | ส่งตัวเลขเป็น string ("2590") |
| ไม่ใส่ tag | ใช้ชื่อ field ตรง ๆ → "UserID" |
// หลาย package ในบรรทัดเดียว คั่นด้วยเว้นวรรค:
UserID int `json:"userId" xml:"userId" validate:"required"`
กฎที่ทำให้ข้อมูลหายแบบไม่มี error
| สถานการณ์ | ผล | มี error ไหม |
| field ขึ้นต้นตัวเล็ก | ข้ามทั้งขาเข้าและขาออก | ❌ เงียบสนิท |
omitempty + ค่าเป็น 0/false | หายไปจาก JSON | ❌ |
| JSON ไม่ส่ง field มา | ได้ zero value | ❌ |
| JSON มี field ส่วนเกิน | ทิ้งไปเฉย ๆ | ❌ |
key ตัวพิมพ์ไม่ตรง ("ID" vs "id") | เข้าได้ (case-insensitive) | ❌ |
| type ไม่ตรง | ไม่เข้า | ✅ มี error |
ลืม & ตอน Unmarshal | ไม่เข้า | ✅ Unmarshal(non-pointer …) |
Pointer แยก "ไม่ส่ง" ออกจาก "ส่งค่า zero"
type Item struct {
Price *int `json:"price,omitempty"`
Active *bool `json:"active,omitempty"`
}
Item{} // {}
Item{Price: &price, Active: &active} // {"price":0,"active":false}
ปกป้องข้อมูลลับ — แยก struct ปลอดภัยกว่า tag
type UserModel struct { ID int; Email string; PasswordHash string } // ในระบบ
type UserResponse struct { // ส่งออก
ID int `json:"id"`
Email string `json:"email"`
}
Google JSON Style Guide (ที่คอร์สอ้างถึง)
- property name เป็น
camelCase, ASCII
- ใช้ double quote เท่านั้น · ห้ามมี comment ใน JSON
- วันที่ใช้รูปแบบ RFC 3339 —
2007-11-06T16:34:41.000Z
- ค่าเป็นได้แค่ boolean, number, string, object, array, null
7 · Encoder / Decoder — เมื่อ Marshal ไม่พอ
| Marshal/Unmarshal | Encoder/Decoder |
| ทำงานกับ | []byte ในมือ | io.Reader/io.Writer (stream) |
| หน่วยความจำ | เท่าขนาดข้อมูลทั้งก้อน | คงที่ |
| เหมาะกับ | ข้อมูลเล็ก ที่มีอยู่แล้ว | ไฟล์ใหญ่, HTTP body, network |
| โหมดเข้มงวด | ❌ | ✅ DisallowUnknownFields() |
file, err := os.Open("product.json")
if err != nil { return err }
defer file.Close()
decoder := json.NewDecoder(file)
decoder.DisallowUnknownFields() // → json: unknown field "isAdmin"
var product Product
if err := decoder.Decode(&product); err != nil { return err }
// ฝั่งเขียนออก — ไม่ต้องสร้าง []byte กลาง:
json.NewEncoder(w).Encode(response) // w คือ http.ResponseWriter ก็ได้
- เปิด
DisallowUnknownFields() สำหรับ config
และ request body ของเราเอง — จับ typo ได้
- อย่าเปิด ตอนอ่าน response จาก API ภายนอก —
เขาเพิ่ม field ใหม่เมื่อไหร่ระบบเราพังทันที
JSON ที่ไม่รู้โครงสร้าง
var m map[string]any
json.Unmarshal(data, &m)
m["id"] // 101 แต่ %T = float64 ← ตัวเลขทุกตัวเป็น float64!
m["tags"] // %T = []interface{}
8 · ตารางกับดักรวม — อ่านก่อนส่ง PR
| โค้ดที่เห็น | สิ่งที่คุณคิด | สิ่งที่เกิดจริง |
var v float64 = score (Score) |
ข้างใต้เป็น float64 ก็น่าจะได้ |
💥 compile error · ต้อง float64(score) |
type T = U แล้วแปะ method |
ได้ method บน type ใหม่ |
💥 cannot define new methods on non-local type |
var i I = T{} เมื่อ method ใช้ *T |
Go แปลงให้เอง |
💥 method has pointer receiver · ใส่ & |
s := i.(string) |
ไม่ตรงก็ได้ค่าว่าง |
💥 panic · ใช้ s, ok := … |
fmt.Sprintf("%v", p) ใน String() |
พิมพ์ค่า struct |
💥 วนไม่จบ → stack overflow |
fmt.Errorf("...: %v", err) |
ห่อ error แล้ว |
โซ่ขาด · errors.Is = false เงียบ ๆ |
custom error ไม่มี Unwrap() |
field Err พอแล้ว |
errors.Is มองไม่ทะลุ |
return e เมื่อ e คือ *AppError ที่เป็น nil |
ผู้เรียกได้ nil |
💥 err != nil เป็น true (typed nil) |
err.Error() == "user not found" |
เช็ค error ได้ |
พังทันทีที่มีคนแก้ข้อความ · ใช้ errors.Is |
field password string + tag |
tag ทำให้ออก JSON ได้ |
หายเงียบทั้งสองทาง (unexported) |
Price int `json:",omitempty"` ค่า 0 |
ส่ง 0 ออกไป |
หายไปจาก JSON · ใช้ *int |
JSON ส่ง "isAdmin" เกินมา |
ได้ error เตือน |
ทิ้งเงียบ · ต้อง DisallowUnknownFields() |
json.Unmarshal(data, product) |
แก้ product ได้ |
Unmarshal(non-pointer …) · ใส่ & |
m["id"] จาก map[string]any |
ได้ int |
ได้ float64 ทุกตัวเลข |
func f() error ขึ้นบรรทัดใหม่ค่อย { |
จัดรูปแบบสวยงาม |
💥 unexpected semicolon or newline before { |