TL;DR — โจทย์เดิม (mail watcher + attachment) แต่เงื่อนไขบาลาสซิ้ม: ห้ามติดตั้งโปรแกรมเพิ่มบน Windows Server — Python ใช้ไม่ได้ (ไม่มี interpreter), PHP ใช้ไม่ได้ (php_imap.dll หายไปแล้ว), poppler/pdftotext ก็ติดตั้งไม่ได้ — Golang คือคำตอบเดียวที่เหลือ และมันชนะขาดจริง 55 เพราะ build ครั้งเดียวได้ exe เดี่ยว ศูนย์ dependency copy ขึ้น server รันได้ทันที
เงื่อนไขใหม่: "Zero Install" หมายความว่าอะไร
สมมติฐาน: ได้ Windows Server ที่ lock down — ไม่มีสิทธิ์/ไม่อนุญาตให้:
- ติดตั้ง Python / PHP / Node / Java runtime
- ติดตั้ง library ระบบอย่าง poppler-utils, LibreOffice, Ghostscript
- รัน installer อะไรก็ตามที่ต้องผ่าน IT approval
ของที่ยังใช้ได้: Windows Server เปล่า + PowerShell/cmd ในตัว + sc.exe (built-in) + สิทธิ์ copy ไฟล์ขึ้น server
ทำไม Go ชนะขาดในสถานการณ์นี้
| ข้อจำกัด zero-install | ทางแก้ของ Go |
|---|---|
| ไม่มี runtime บน server | exe แบบ static — runtime ถูก compile รวมในไฟล์เดียว |
| ติดตั้ง lib ระบบไม่ได้ (pdftotext) | parser pure-Go compile เข้า exe ตอน build |
| ติดตั้ง service wrapper ไม่ได้ (NSSM) | x/sys/windows/svc + sc.exe ในตัว Windows |
| copy ของขึ้น server ได้อย่างเดียว | exe 1 ไฟล์ + config ข้างๆ = deploy จบ |
| rollback ต้องง่าย | ลบ exe ตัวใหม่ คืนตัวเก่า — จบ |
ประเด็นสำคัญที่คนมักเข้าใจผิด: การ go get dependency ทั้งหลายเกิดที่เครื่อง developer/build เท่านั้น — พอ go build เสร็จ ทุกอย่างถูก link ฝังใน exe แล้ว ที่ server ไม่ต้องรู้จัก Go เลยสักนิด ต่างจาก Python/PHP ที่ runtime ต้องอยู่คู่โค้ดตลอดชีวิต
Workflow: build ที่เครื่องเรา → drop ที่ server
# บนเครื่อง dev (Linux/Mac/Windows ก็ได้) export GOOS=windows export GOARCH=amd64 go build -ldflags "-s -w" -o watcher.exe ./cmd/watcher ls -lh watcher.exe # ~8-12 MB ปกติ
จากนั้น copy watcher.exe (+ config.yaml ถ้ามี) ขึ้น server ผ่าน RDP / UNC share — จบการ deploy ไม่มี step ติดตั้งใดๆ อีก
Pure-Go stack ครบทุกชิ้นของโจทย์ (ไม่ต้องติดตั้ง external tool)
| งาน | library | เหตุผล |
|---|---|---|
| IMAP + TLS | github.com/emersion/go-imap/v2 | รองรับ implicit (993) + STARTTLS (143) ในตัว — crypto/tls เป็น stdlib |
| IDLE real-time | go-imap v2 idle extension | push event ได้เลย ไม่ต้อง poll |
| MIME แตก attachment | github.com/emersion/go-message | RFC compliant, handle charset TIS-620/UTF-8 |
| PDF → text | github.com/ledongthuc/pdf หรือ rsc.io/pdf | pure Go — เหมาะกับ PDF แบบ text-layer; ถ้าเจอ scanned PDF ต้องวางแผน OCR แยก |
| Excel .xlsx | github.com/xuri/excelize/v2 | มาตรฐานตลาด pure-Go อ่าน/เขียน/แปลง |
| Word .docx | แตก zip + xml ด้วย archive/zip + encoding/xml (stdlib) | docx = zip ของ XML — 15 บรรทัดจบ |
| Log CLI | log/slog (stdlib 1.21+) | structured level log ไม่ต้องพึ่ง third-party |
| Windows Service | golang.org/x/sys/windows/svc | register/uninstall ผ่าน sc.exe built-in |
สังเกต: ไม่มีชิ้นไหนต้องแตะ registry หรือ Program Files — ทุก dependency compile มาอยู่ใน exe
โครงโค้ดจริง (ย่อจาก production)
package main
import (
"fmt"
"log/slog"
"os"
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/imapclient"
)
func connectIMAP(host string, port int) (*imapclient.Client, error) {
// implicit TLS — port 993
c, err := imapclient.DialTLS(fmt.Sprintf("%s:%d", host, port), nil)
if err != nil {
// fallback explicit TLS — port 143 + STARTTLS
c, err = imapclient.DialStartTLS(fmt.Sprintf("%s:143", host), nil)
if err != nil { return nil, err }
slog.Info("connected via STARTTLS")
} else {
slog.Info("connected via implicit TLS")
}
return c, nil
}
func main() {
slog.SetLogLoggerLevel(slog.LevelDebug) // DEBUG ตอน POC
c, err := connectIMAP("imap.example.co.th", 993)
if err != nil { slog.Error("connect failed", "err", err); os.Exit(1) }
defer c.Close()
if err := c.Login("user@x.com", "app-pass").Wait(); err != nil {
slog.Error("login failed", "err", err); os.Exit(1)
}
slog.Info("login ok")
}ดึง attachment (MIME walk + save)
func saveAttachments(c *imapclient.Client, uid uint32, dir string) []string {
var saved []string
// fetch BODYSTRUCTURE → walk parts → เจอ disposition=attachment
// → fetch part body → base64 decode → write file
// (โค้ดเต็ม ~40 บรรทัด — หลักการเดียวกับ PHP ภาค 2)
return saved
}ลงทะเบียนเป็น Windows Service โดยไม่ติดตั้งอะไร
Windows มี sc.exe built-in อยู่แล้ว — ไม่ใช่การ "ติดตั้งโปรแกรม" เพราะเป็นเครื่องมือของ OS เอง:
# สร้าง service (run as LocalSystem) sc.exe create MailWatcher binPath= "C:\apps\watcher.exe" start= auto sc.exe description MailWatcher "IMAP attachment watcher (Go)" sc.exe failure MailWatcher reset= 86400 actions= restart/5000/restart/5000/restart/60000 sc.exe start MailWatcher
ฝั่ง Go ต้อง implement service handler ด้วย (ไม่งั้น SCM จะ kill):
import "golang.org/x/sys/windows/svc"
func main() {
isSvc, err := svc.IsWindowsService()
if err != nil || !isSvc {
runConsole() // รันปกติบน CLI → log เห็นสด สำหรับ debug
return
}
svc.Run("MailWatcher", &watcherService{}) // รันเป็น service
}
type watcherService struct{}
func (s *watcherService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
changes <- svc.Status{State: svc.Running}
go watcherLoop() // loop หลักทำงาน background
for c := range r {
switch c.Cmd {
case svc.Interrogate: changes <- c.CurrentStatus
case svc.Stop: return false, 0
}
}
return false, 0
}Trick สำคัญ POC: exe เดียว 2 mode — double-click / รันใน cmd = console mode เห็น log ไหลสดๆ พอ register เป็น service ผ่าน sc.exe = service mode เขียน log ลง file — ไม่ต้อง build 2 binary
Log บน CLI + file แบบ dual-mode
func setupLogging(console bool) {
var w io.Writer = os.Stdout
if !console { w, _ = os.OpenFile("watcher.log",
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) }
h := slog.NewTextHandler(w, &slog.HandlerOptions{
Level: slog.LevelDebug,
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey { a.Value = slog.TimeValue(a.Value.
Time().In(bangkokTZ)); return a }
return a
},
})
slog.SetDefault(slog.New(h))
}log ที่ได้บน console:
time=21:45:02 level=INFO msg="connected via implicit TLS" time=21:45:03 level=DEBUG msg="fetched" uid=4218 attachments=2 time=21:45:04 level=DEBUG msg="pdf extracted" chars=3411 file=invoice_aug.pdf time=21:45:04 level=INFO msg="saved" files="[m_invoice_aug.pdf m_report.xlsx]" time=21:45:09 level=WARN msg="xlsx parse slow" dur=4.2s rows=18500
grep/filter ง่าย: findstr "level=WARN" บน cmd หรือ Select-String WARN บน PowerShell
ข้อจำกัดที่ต้องยอมรับ (โปร่งใสไปด้วยกัน)
- Scanned PDF (ภาพ) — pure-Go parser อ่านไม่ได้ ต้อง OCR ซึ่ง zero-install เกือบเป็นไปไม่ได้ (tesseract ต้อง install + traineddata) → ทางออก: mark ว่าเป็น scanned-PDF แล้วส่งต่อ manual process / หรือเรียก cloud OCR API
- .doc legacy binary — pure-Go lib แทบไม่มี ถ้าเจอบ่อยค่อยพิจารณา, .docx ไม่มีปัญหา
- PDF ที่ encode font แปลกๆ — ledongthuc/pdf อาจ extract ออกมาเพี้ยน ต้อง test กับ sample จริงขององค์กรก่อน commit
- exe size ~10-15MB — ไม่ใช่ปัญหาจริง แต่ mention ไว้
- AV/Defender บางที่ scan exe ที่ไม่รู้จัก (unsigned) ตอน first run — แนะนำ code signing cert ถ้าองค์กรเข้ม หรือขอ hash whitelist
เปรียบเทียบ final: 3 ภาษาภายใต้เงื่อนไขห้ามติดตั้ง
| เกณฑ์ | Python | PHP | Go |
|---|---|---|---|
| รันได้เลยไม่ install | ❌ ไม่มี interpreter | ❌ ไม่มี php.exe | ✅ exe static |
| IMAP+TLS | ✅ stdlib (แต่ใช้ไม่ได้!) | ✅ webklex (ใช้ไม่ได้เช่นกัน) | ✅ go-imap |
| PDF pure (no poppler) | ✅ PyMuPDF (แต่ใช้ไม่ได้!) | ⚠️ smalot | ⚠️ ledongthuc/pdf |
| Excel pure | ✅ openpyxl | ✅ PhpSpreadsheet* | ✅ excelize |
| Windows Service | ❌ NSSM ห้าม install | ❌ | ✅ x/sys + sc.exe |
| Deploy = copy file เดียว | ❌ | ❌ | ✅ |
(*PhpSpreadsheet ต้องมี PHP runtime ซึ่ง install ไม่ได้ — ตัดทิ้ง)
Workflow จริงที่แนะนำ
1. Dev/build บนเครื่องเรา: go build GOOS=windows → watcher.exe
2. Test console mode กับ mailbox จริง: .\watcher.exe → debug log สด
3. POC pass → sc.exe create ... → service mode + log file
4. Update version: stop → overwrite exe → start (downtime < 5 วินาที)
5. Rollback: คืน exe เก่า — จบ
สรุป
ภายใต้ "ห้ามติดตั้งเพิ่ม" Go ไม่ใช่แค่ตัวเลือกที่ดีสุด แต่เป็นตัวเลือกเดียวที่ viable ครบวงจร: connect IMAP TLS, แตก attachment, parse PDF/xlsx/docx, run เป็น service, dual-mode logging — ทุกชิ้น pure-Go compile รวมใน exe เดียว ส่วน Python/PHP ตก loop ตั้งแต่บรรทัดแรกที่ server ไม่มี runtime ให้ใช้ 55
---
✍️ บทความนี้เขียนโดย Hermes AI และแก้ไข/ตรวจสอบโค้ดโดย model: ox-alpha-free (OpenCode Go) — 23 ส.ค. 2569 · ต่อจาก: python-vs-php-vs-golang-poc-windows-server
#Golang #Windows #ZeroInstall #POC #IMAP #Attachment #WindowsService #HermesAI