รับมือกับปัญหา Import Cycles ใน Golang ด้วยการใช้ Interface

Thanthai Jitprathum
odds.team
Published in
2 min readJun 23, 2024

สืบเนื่องมาจากว่า มีเพื่อนผมท่านหนึ่งติดปัญหาเกี่ยวกับการ Import ข้าม Package ทำให้ผมมีโอกาสได้หาข้อมูลวิธีการแก้ปัญหาดังกล่าวช่วยเพื่อน และบทความนี้เป็นผลพลอยได้ของการหาข้อมูลนั้นครับ 🎉

ในฐานะนักพัฒนา Golang เราคุ้นเคยกับการจัดการโค้ดด้วยการแบ่งเป็น Package เพื่อความเป็นระเบียบเรียบร้อย แต่บางครั้งการแยก Package อาจนำไปสู่ปัญหาที่เรียกว่า Import Cycles

Import Cycles

ลองนึกภาพว่าเรามีสอง Package คือ a และ b

กรณีที่ Package a จำเป็นต้องใช้ฟังก์ชันหรือตัวแปรภายใน Package b ดังนั้น a จึง import Package b

ในขณะเดียวกัน Package b ก็จำเป็นต้องใช้ฟังก์ชันหรือตัวแปรภายใน Package a ด้วยเช่นกัน ทำให้ b import Package a กลับคืนด้วย

สถานการณ์นี้ก่อให้เกิด Import Cycles ขึ้น ซึ่งเป็นสิ่งที่ภาษา Go ไม่อนุญาต

Import Cycles ไม่ได้เกิดขึ้นแค่กรณีง่ายๆ แบบนี้เสมอไป ตัวอย่างเช่น Package b อาจจะไม่ได้ Import Package aโดยตรง แต่ Package b Import Package c และ Package c กลับไป Import Package a สุดท้ายก็ยังคงเป็น Import Cycles อยู่ดี

มาดูตัวอย่างโค้ดที่ทำห้เกิด Import Cycles กันดีกว่า

Package a:

package a

import (
"import-cycle-example/b"
)

func A() {
b.B()
}

Package b:

package b

import (
"import-cycle-example/a"
)

func B() {
a.A()
}

Import Cycles Are Bad Design

ภาษา Go มุ่งเน้นไปที่การ Compile ที่รวดเร็ว ซึ่งเร็วมากกว่าความเร็วในการ Execute โปรแกรมด้วยซ้ำ แม้ว่าจะต้องสละประสิทธิภาพรันไทม์บางส่วนก็ตาม

ดังนั้น Go Complier จึงไม่เสียเวลากับการสร้างพยายามสร้าง Machine Code ให้ประสิทธิภาพสูงสุดเท่าที่จะเป็นไปได้ แต่หันมาให้ความสำคัญกับการ Compile โค้ดจำนวนมากให้ได้อย่างรวดเร็วแทน

การอนุญาตให้มี Cyclic/Circular ระหว่าง Package ใน Go จะส่งผลเสียให้เวลา Compile นานขึ้น เพราะทุกครั้งที่มีการเปลี่ยนแปลงใน Package ใดๆ ภายในวงจร Package อื่นๆ ที่เกี่ยวข้องทั้งหมดจะต้องถูก Compile ใหม่ด้วย และอาจเกิด Recursion ไม่สิ้นสุดได้ในบางกรณี และอาจทำให้เกิดการเรียกตัวเองซ้ำไม่รู้จบ

เท่านั้นยังไม่พอ ยังส่งผลให้เกิด Memory Leaks ได้อีกด้วย เนื่องจาก Object ภายใน Package ต่างๆ ที่อยู่ในวงจรต่างก็อ้างอิงซึ่งกันและกัน ส่งผลให้จำนวนการอ้างอิงของ Object เหล่านั้นไม่เคยลดลงเป็น 0 เลย และทำให้ Garbage Collector ไม่สามารถเก็บกวาดข้อมูลเหล่านั้นออกไปได้

การแก้ไขปัญหา Import Cycles

เมื่อเราข้อผิดพลาดเกี่ยวกับ Import Cycles สิ่งที่ควรทำคือ ให้ลองหยุดคิดทบทวนเกี่ยวกับการจัดโครงสร้างโปรเจกต์ของเราสักพัก

มีวิธีแก้ไขปัญหานี้หลายวิธี ซึ่งวิธีที่ง่ายและพบมากที่สุดคือการใช้ Interface

ใช้ Interface

  • Package a มีการใช้ functions/variables จาก Package b โดยการ Import Package b
  • Package b มีการใช้ functions/variables จาก Package a โดยไม่ต้อง Import Package a การกระทำทั้งหมดจะต้องทำผ่าน Instance ของ Package a ซึ่งใช้ Interface ที่กำหนดใน b Instance พวกนั้นจะถูกมองว่าเป็น Object ของ Package b

Package b:

package b

import (
"fmt"
)

type pa interface {
HelloFromA()
}

type PB struct {
PA pa
}

func New(pa pa) *PB {
return &PB{
PA: pa,
}
}

func (p *PB) HelloFromB() {
fmt.Println("Hello from package b")
}

func (p *PB) HelloFromASide() {
p.PA.HelloFromA()
}

Package a:

package a

import (
"fmt"
"import-cycle-example/b"
)

type PA struct{}

func New() *PA {
return &PA{}
}

func (p *PA) HelloFromA() {
fmt.Println("Hello from package a")
}

func (p *PA) HelloFromBSide() {
pb := b.New(p)
pb.HelloFromB()
}

Package main:

package main

import (
"import-cycle-example/a"
)

func main() {
pa := a.PA{}
pa.HelloFromBSide()
}

ผมหวังว่าจะแชร์ความเข้าใจนี้ให้ผู้อ่านทุกท่านได้ไม่มากก็น้อย และขอให้มีความสุขกับการโค้ดดิ้งครับ 🎉

--

--