เขียน Go แบบ Java/Ruby/PHP — ทางลัดสู่ Technical Debt โดยไม่รู้ตัว
มีเรื่องเล่าใน community Go ว่าเคยมีคนเขียนโค้ดแบบนี้ส่งมาที่ code review:
type UserGetterServiceInterface interface {
GetUser(id string) (*User, error)
}
type UserGetterService struct {
repo UserRepositoryInterface
}
func NewUserGetterService(repo UserRepositoryInterface) *UserGetterService {
return &UserGetterService{repo: repo}
}
แล้วคนรีวิวถามว่า "พี่ครับ... อะไรคือ UserGetterServiceInterface? แล้วทำไมต้องมี NewUserGetterService?"1
เจ้าตัวตอบแบบซื่อ ๆ ว่า "ก็ผมมาจาก Java นี่นา"
ปัญหาไม่ได้อยู่ที่ภาษา — อยู่ที่ "สำเนียง"
เวลาเราเรียนรู้ภาษาใหม่ — โดยเฉพาะภาษาแรกหรือภาษาที่สอง — เรามักจะเขียนมันด้วย "สำเนียง" ของภาษาเดิม ฝรั่งเรียกปรากฏการณ์นี้ว่า accent-driven development
| ถ้าคุณมาจาก... | คุณมักจะ... |
|---|---|
| ☕ Java | สร้าง interface ทุกอย่าง, factory pattern, getter/setter, inheritance |
| 💎 Ruby | metaprogramming, monkey-patching, magic method, method_missing
|
| 🐘 PHP | global state, framework-first mindset, procedural ใน struct |
| 🐍 Python | exception for control flow, decorator ซ้อน decorator, __init__ magic |
ทั้งหมดนี้ไม่ใช่ของไม่ดี — แต่มันคือ "เครื่องมือของอีกภาษา" พอเอามาใช้ใน Go โดยไม่ปรับ mindset — มันกลายเป็น code ที่อ่านยาก, ช้า, และที่สำคัญคือ ไม่ใช่ Go
☕ Java → Go: OOP Overdose
นี่คือกลุ่มที่เจอบ่อยที่สุด เพราะคนเขียน Java เยอะ และ Go ถูกใช้แทนที่ Java ใน microservices พอดี
Interface ทุกอย่าง — "เผื่อไว้"
// ❌ สำเนียง Java — interface ที่มี implementation เดียว
type UserRepository interface {
FindByID(ctx context.Context, id string) (*User, error)
Save(ctx context.Context, user *User) error
}
type PostgresUserRepo struct { db *sql.DB }
// ... implement ทั้งสอง method
func NewUserService(repo UserRepository) *UserService { ... }
ใน Java คุณต้องมี interface เพราะ DI framework (Spring) ต้องการมัน และเพราะการ mock ใน Java ทำผ่าน interface
ใน Go — interface ถูกใช้ต่างกันโดยสิ้นเชิง:
"The bigger the interface, the weaker the abstraction."2
Go interfaces should be:
- เล็ก (1-3 methods)
- ประกาศฝั่ง consumer (คนใช้), ไม่ใช่ฝั่ง producer (คนสร้าง)
- ใช้เมื่อมีอย่างน้อย 2 implementations — ไม่ใช่ "เผื่อไว้"
// ✅ วิธี Go — ประกาศ interface ที่ consumer
// user_service.go
type userFinder interface {
FindByID(ctx context.Context, id string) (*User, error)
}
type UserService struct {
users userFinder // ← interface เกิดตรงนี้ — เล็ก, ใช้จริง
}
// user_repo.go — ฝั่งนี้ไม่มี interface เลย
type UserRepo struct { db *sql.DB }
func (r *UserRepo) FindByID(ctx context.Context, id string) (*User, error) { ... }
แบบนี้มีข้อดี:
- interface เกิดที่คนใช้ → คนใช้บอกเองว่าอยากได้แค่
FindByIDไม่เอาทั้งSave,Delete,Update3 - ไม่ต้องมี mock framework — แค่สร้าง
struct{ FindByID func(...) }ก็ test ได้ - อ่านโค้ดรู้ทันทีว่า service ใช้แค่ find — ไม่ได้ใช้ save
Getter/Setter ทุก field — "เผื่อมี logic ภายหลัง"
// ❌ สำเนียง Java — getter/setter ทั้งที่ field เป็น public อยู่แล้ว
type User struct {
name string
email string
}
func (u *User) GetName() string { return u.name }
func (u *User) SetName(n string) { u.name = n }
func (u *User) GetEmail() string { return u.email }
func (u *User) SetEmail(e string) { u.email = e }
ใน Java คุณทำแบบนี้เพราะ:
- Bean convention — framework จะเรียก
getXxx()/setXxx()อัตโนมัติ - Encapsulation — เผื่อมี logic ใน getter/setter วันข้างหน้า
ใน Go — YAGNI4:
// ✅ Go — field คือ field, อย่า wrap โดยไม่จำเป็น
type User struct {
Name string
Email string
}
ถ้าวันข้างหน้ามี logic จริง ๆ — ค่อยเปลี่ยนเป็น method ตอนนั้น compiler จะฟ้องทุกที่ที่ใช้ user.Name ทำให้ refactor ปลอดภัย
"Architecture" ก่อนเขียนโค้ด
// ❌ สำเนียง Java — โครงสร้างโฟลเดอร์ที่มาก่อนปัญหา
myapp/
├── controller/
│ └── user_controller.go
├── service/
│ ├── user_service.go
│ └── user_service_interface.go
├── repository/
│ ├── user_repository.go
│ └── user_repository_interface.go
├── model/
│ └── user.go
├── dto/
│ └── user_dto.go
└── util/
└── string_util.go
สำหรับ Go — เริ่มจาก flat structure ก่อน:
// ✅ Go — เริ่มเรียบ ๆ, แยกเมื่อจำเป็น
myapp/
├── main.go
├── user.go // User struct + UserRepo + UserService
├── user_test.go
└── handler.go // HTTP handlers
พอโปรเจกต์ใหญ่ขึ้นก็ค่อยแยก package — แต่ไม่ใช่เพราะ "architecture บอกว่าต้องแยก" — แต่เพราะโค้ดเรียกร้องให้แยก5
💎 Ruby → Go: Magic มากเกิน
คน Ruby โดยเฉพาะ Rails dev — คุ้นเคยกับ "convention over configuration" และ metaprogramming
method_missing ใน Go — ไม่มีวันได้
# Ruby — dynamic method
class User
def method_missing(name, *args)
if name.to_s.start_with?("find_by_")
field = name.to_s.sub("find_by_", "")
# ... query database
end
end
end
User.new.find_by_email("a@b.com") # Magic!
ใน Go — ไม่มีทาง:
// Go — explicit methods เท่านั้น
func (r *UserRepo) FindByEmail(ctx context.Context, email string) (*User, error) {
// เขียนเองทีละ method — ชัดเจน, grep ได้, IDE autocomplete ได้
}
หลายคนที่ย้ายมาจาก Ruby จะรู้สึกว่า Go "verbose" — แต่นั่นคือ feature ไม่ใช่ bug: โค้ด Go อ่านแล้วรู้ทันทีว่าทำอะไร โดยไม่ต้องรันก่อน
nil ≠ nil — หลุมพรางที่ Ruby dev มองข้าม
ใน Ruby — nil คือ nil:
user = nil
puts user.name if user # ไม่พัง
ใน Go — nil มี type:
// ❌ bug คลาสสิก
func GetUser() *User {
return nil // ← nil pointer to User
}
func GetError() error {
return (*MyError)(nil) // ← ไม่ใช่ nil error! เป็น non-nil interface
}
// เทียบกัน
u := GetUser()
fmt.Println(u == nil) // true
e := GetError()
fmt.Println(e == nil) // false! — เพราะ interface มี type แต่ว่า nil
นี่คือหลุมพรางที่ Ruby dev เจอเกือบทุกคน — interface ใน Go ประกอบด้วย (type, value) ไม่ใช่แค่ value6
🐘 PHP → Go: Frameworks First
ทุกอย่างต้องมี framework
// PHP — Laravel
Route::get('/users/{id}', [UserController::class, 'show']);
// ❌ สำเนียง PHP — ตามหา "Go framework" ที่เหมือน Laravel
import "github.com/someone/goravel"
ใน Go — stdlib net/http ก็พอแล้ว:
// ✅ Go — router ใน 3 บรรทัด
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", handleGetUser)
http.ListenAndServe(":8080", mux)
Go 1.22+ รองรับ method-based routing ใน stdlib แล้ว ไม่ต้องหา framework เล็ก ๆ มาทำเรื่องนี้7
Global state
PHP dev คุ้นเคยกับการใช้ global:
// PHP
$db = new PDO("...");
function getUser($id) {
global $db;
return $db->query("...");
}
ใน Go — injection คือคำตอบ:
// ✅ Go — dependency injection ผ่าน constructor ง่าย ๆ
type Handler struct {
svc *UserService
}
func NewHandler(svc *UserService) *Handler {
return &Handler{svc: svc}
}
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
user, err := h.svc.GetUser(r.Context(), "42")
// ...
}
ไม่ต้องมี DI framework — แค่ส่ง *UserService เข้า constructor ก็ได้ละ
🐍 Python → Go: Exception Mindset
Error คือ Exception
# Python — try/except
def get_user(id):
try:
return db.query(f"SELECT * FROM users WHERE id = {id}")
except DatabaseError:
return None
// ❌ สำเนียง Python — panic แทน return error
func GetUser(id string) *User {
user, err := db.Query(...)
if err != nil {
panic(err) // ❌ อย่าทำ! Go ไม่ได้ใช้ panic เป็น control flow
}
return user
}
Go — error คือ value:
// ✅ Go — return error, ให้ caller จัดการ
func GetUser(ctx context.Context, id string) (*User, error) {
user, err := db.QueryRow(ctx, "SELECT * FROM users WHERE id = $1", id)
if err != nil {
return nil, fmt.Errorf("GetUser: %w", err)
}
return user, nil
}
Decorator ซ้อน Decorator
# Python
@login_required
@validate_params
@cache_result(ttl=300)
def get_user(id):
...
// ✅ Go — middleware chain — ชัดเจนว่าอะไรเกิดก่อน-หลัง
func handleGetUser(w http.ResponseWriter, r *http.Request) { ... }
// ใน main:
handler := middleware.Chain(
handleGetUser,
middleware.LogRequest,
middleware.RequireAuth,
middleware.ValidateParams,
middleware.CacheResult(5*time.Minute),
)
ต่างกันที่ — ใน Go คุณเห็นลำดับชัดเจน (ข้างใน → ข้างนอก) — ใน Python มันซ้อนกันจากบนลงล่าง ทำให้ debug ลำบากเวลามี middleware หลายตัว
สรุป — เรียนรู้ Grammar ไม่พ้อง ต้องเรียนรู้ "สำเนียง"
| สำเนียงเดิม | อาการที่พบบ่อย | ทางแก้ |
|---|---|---|
| ☕ Java | interface ทั้งที่ใช้ implementation เดียว | interface เล็ก ประกาศที่ consumer |
| ☕ Java | getter/setter ครอบ field เปล่า ๆ | field คือ field, refactor ทีหลัง |
| ☕ Java | โฟลเดอร์แยกตาม layer ก่อนเขียนโค้ด | เริ่ม flat, แยกเมื่อจำเป็น |
| 💎 Ruby | พยายาม metaprogramming | Go = explicit, embrace it |
| 💎 Ruby |
nil ไม่ระวัง type |
เข้าใจว่า interface nil ≠ pointer nil |
| 🐘 PHP | ตามหา framework | net/http พอ, Go 1.22+ มี routing ในตัว |
| 🐘 PHP | global state | dependency injection ผ่าน constructor |
| 🐍 Python | panic = exception | error คือ value, return มัน |
| 🐍 Python | decorator ซ้อน decorator | middleware chain — ชัดเจนกว่า |
วิธีแก้ — อ่าน Effective Go
หลังจากเขียน Go มาได้สักเดือน — ผมแนะนำให้อ่าน Effective Go หนึ่งรอบ
มันคือ "สำเนียง" ทางการของ Go — ไม่ใช่แค่ syntax — แต่มันสอนวิธีคิด: ทำไมใช้ := ตรงนี้, ทำไม interface ต้องเล็ก, ทำไม init() ใช้แค่เมื่อจำเป็น
และอีกเรื่องที่ช่วยได้มาก: อ่านโค้ด standard library8 — net/http, encoding/json, database/sql — โค้ดพวกนี้คือตัวอย่างของ "สำเนียง Go" ที่ดีที่สุด
เชิงอรรถ
-
"NewXxx" vs "xxxBar": ใน Go — constructor มักชื่อ
NewXxx— แต่ใช้เฉพาะเมื่อมันมี logic เช่น validate input หรือตั้งค่า default — ไม่ใช่แค่return &Xxx{}เปล่า ๆ ↩ -
"The bigger the interface, the weaker the abstraction" — Rob Pike, Go Proverbs — https://go-proverbs.github.io ↩
-
Interface ที่ consumer: นี่คือหนึ่งในแนวคิดที่แตกต่างจาก Java มากที่สุด — ใน Java, interface ถูกประกาศโดย library owner ใน Go, interface ถูกประกาศโดย library user — ทำให้ dependency กลับทิศ, สะอาดกว่า ↩
-
YAGNI: "You Aren't Gonna Need It" — หลักการออกแบบที่บอกว่าอย่าเขียนโค้ดสำหรับ requirement ที่ยังไม่มี — เพราะ requirement มักเปลี่ยน หรือไม่เกิดเลย ↩
-
Package structure: มี talk ชื่อดังโดย Ben Johnson ชื่อ "Standard Package Layout" — https://www.gobeyond.dev/standard-package-layout/ ↩
-
Interface nil ≠ pointer nil: interface ใน Go เก็บ (type, value) เป็น tuple —
(*MyError)(nil)มี type =*MyErrorแต่ value = nil — interface โดยรวมไม่ใช่ nil อ่านเพิ่ม: https://go.dev/doc/faq#nil_error ↩ -
Go 1.22 routing:
http.ServeMuxรองรับGET /users/{id}ตั้งแต่ Go 1.22 — https://go.dev/blog/routing-enhancements ↩ -
อ่าน standard library: Go standard library คือ textbook ที่ดีที่สุด — เขียนโดยคนเดียวกันกับที่ออกแบบภาษา มันคือ "สำเนียง Go" ที่ถูกต้องและสะอาดที่สุดที่มีอยู่ ↩
Top comments (0)