DEV Community

Gophernment Co
Gophernment Co

Posted on

เขียน Go แบบ Java/Ruby/PHP — ทางลัดสู่ Technical Debt โดยไม่รู้ตัว

เขียน 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}
}
Enter fullscreen mode Exit fullscreen mode

แล้วคนรีวิวถามว่า "พี่ครับ... อะไรคือ 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 { ... }
Enter fullscreen mode Exit fullscreen mode

ใน 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) { ... }
Enter fullscreen mode Exit fullscreen mode

แบบนี้มีข้อดี:

  • 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 }
Enter fullscreen mode Exit fullscreen mode

ใน Java คุณทำแบบนี้เพราะ:

  1. Bean convention — framework จะเรียก getXxx() / setXxx() อัตโนมัติ
  2. Encapsulation — เผื่อมี logic ใน getter/setter วันข้างหน้า

ใน Go — YAGNI4:

// ✅ Go — field คือ field, อย่า wrap โดยไม่จำเป็น
type User struct {
    Name  string
    Email string
}
Enter fullscreen mode Exit fullscreen mode

ถ้าวันข้างหน้ามี 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
Enter fullscreen mode Exit fullscreen mode

สำหรับ Go — เริ่มจาก flat structure ก่อน:

// ✅ Go — เริ่มเรียบ ๆ, แยกเมื่อจำเป็น
myapp/
├── main.go
├── user.go          // User struct + UserRepo + UserService
├── user_test.go
└── handler.go       // HTTP handlers
Enter fullscreen mode Exit fullscreen mode

พอโปรเจกต์ใหญ่ขึ้นก็ค่อยแยก 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!
Enter fullscreen mode Exit fullscreen mode

ใน Go — ไม่มีทาง:

// Go — explicit methods เท่านั้น
func (r *UserRepo) FindByEmail(ctx context.Context, email string) (*User, error) {
    // เขียนเองทีละ method — ชัดเจน, grep ได้, IDE autocomplete ได้
}
Enter fullscreen mode Exit fullscreen mode

หลายคนที่ย้ายมาจาก Ruby จะรู้สึกว่า Go "verbose" — แต่นั่นคือ feature ไม่ใช่ bug: โค้ด Go อ่านแล้วรู้ทันทีว่าทำอะไร โดยไม่ต้องรันก่อน

nilnil — หลุมพรางที่ Ruby dev มองข้าม

ใน Ruby — nil คือ nil:

user = nil
puts user.name if user  # ไม่พัง
Enter fullscreen mode Exit fullscreen mode

ใน 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
Enter fullscreen mode Exit fullscreen mode

นี่คือหลุมพรางที่ Ruby dev เจอเกือบทุกคน — interface ใน Go ประกอบด้วย (type, value) ไม่ใช่แค่ value6


🐘 PHP → Go: Frameworks First

ทุกอย่างต้องมี framework

// PHP — Laravel
Route::get('/users/{id}', [UserController::class, 'show']);
Enter fullscreen mode Exit fullscreen mode
// ❌ สำเนียง PHP — ตามหา "Go framework" ที่เหมือน Laravel
import "github.com/someone/goravel"
Enter fullscreen mode Exit fullscreen mode

ใน Go — stdlib net/http ก็พอแล้ว:

// ✅ Go — router ใน 3 บรรทัด
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", handleGetUser)
http.ListenAndServe(":8080", mux)
Enter fullscreen mode Exit fullscreen mode

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("...");
}
Enter fullscreen mode Exit fullscreen mode

ใน 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")
    // ...
}
Enter fullscreen mode Exit fullscreen mode

ไม่ต้องมี 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
Enter fullscreen mode Exit fullscreen mode
// ❌ สำเนียง Python — panic แทน return error
func GetUser(id string) *User {
    user, err := db.Query(...)
    if err != nil {
        panic(err) // ❌ อย่าทำ! Go ไม่ได้ใช้ panic เป็น control flow
    }
    return user
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Decorator ซ้อน Decorator

# Python
@login_required
@validate_params
@cache_result(ttl=300)
def get_user(id):
    ...
Enter fullscreen mode Exit fullscreen mode
// ✅ 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),
)
Enter fullscreen mode Exit fullscreen mode

ต่างกันที่ — ใน 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 library8net/http, encoding/json, database/sql — โค้ดพวกนี้คือตัวอย่างของ "สำเนียง Go" ที่ดีที่สุด


เชิงอรรถ


  1. "NewXxx" vs "xxxBar": ใน Go — constructor มักชื่อ NewXxx — แต่ใช้เฉพาะเมื่อมันมี logic เช่น validate input หรือตั้งค่า default — ไม่ใช่แค่ return &Xxx{} เปล่า ๆ 

  2. "The bigger the interface, the weaker the abstraction" — Rob Pike, Go Proverbs — https://go-proverbs.github.io 

  3. Interface ที่ consumer: นี่คือหนึ่งในแนวคิดที่แตกต่างจาก Java มากที่สุด — ใน Java, interface ถูกประกาศโดย library owner ใน Go, interface ถูกประกาศโดย library user — ทำให้ dependency กลับทิศ, สะอาดกว่า 

  4. YAGNI: "You Aren't Gonna Need It" — หลักการออกแบบที่บอกว่าอย่าเขียนโค้ดสำหรับ requirement ที่ยังไม่มี — เพราะ requirement มักเปลี่ยน หรือไม่เกิดเลย 

  5. Package structure: มี talk ชื่อดังโดย Ben Johnson ชื่อ "Standard Package Layout" — https://www.gobeyond.dev/standard-package-layout/ 

  6. Interface nil ≠ pointer nil: interface ใน Go เก็บ (type, value) เป็น tuple — (*MyError)(nil) มี type = *MyError แต่ value = nil — interface โดยรวมไม่ใช่ nil อ่านเพิ่ม: https://go.dev/doc/faq#nil_error 

  7. Go 1.22 routing: http.ServeMux รองรับ GET /users/{id} ตั้งแต่ Go 1.22 — https://go.dev/blog/routing-enhancements 

  8. อ่าน standard library: Go standard library คือ textbook ที่ดีที่สุด — เขียนโดยคนเดียวกันกับที่ออกแบบภาษา มันคือ "สำเนียง Go" ที่ถูกต้องและสะอาดที่สุดที่มีอยู่ 

Top comments (0)