---
id: programming-go
title: "Go cho CLI một binary"
shortTitle: "Go"
track: programming
trackTitle: "Programming cho DevOps"
order: 3
level: core
status: published
estimatedMinutes: 45
prerequisites:
  - programming-nodejs
next: programming-ruby
reviewedAgainst: 2026-10-12
sources:
  - title: "Tutorial: Getting started"
    url: https://go.dev/doc/tutorial/getting-started
  - title: "flag"
    url: https://pkg.go.dev/flag
  - title: "os/exec"
    url: https://pkg.go.dev/os/exec
  - title: "net/http"
    url: https://pkg.go.dev/net/http
---

# Go cho CLI một binary

Bạn cần cài Go trước và kiểm tra bằng `go version`. Lab không pin bản patch cụ thể, vì module và các package dưới đây đã có từ lâu và vẫn là API hiện hành trong docs trên pkg.go.dev.

## Để làm gì

Bạn đóng tool thành một binary duy nhất, nên máy đích không cần cài Python hay Node. Chỉ cần copy file lên server hoặc nhét vào image rồi chạy. kubectl, terraform và nhiều tool cloud-native khác được phát hành theo đúng cách này.

## Gồm những gì

- Module: `go mod init`.
- Package `flag` cho argument.
- `net/http` với `Client.Timeout`.
- `os/exec` để chạy lệnh khác và lấy exit code của nó.

Bài này không nói về goroutine, framework web, hay cách đưa binary vào Docker image. Phần image nằm ở bài Dockerfile.

## Dùng khi nào

Chọn Go khi tool phải chạy trên máy không có runtime sẵn, hoặc khi nhiều người cần dùng chung một bản build. Với script gắn chặt vào một repo, nơi CI đã có interpreter, Python và Node vẫn tiện hơn.

Ruby chỉ có một bài ngắn. Bạn chủ yếu cần nhận ra nó khi đọc Chef hoặc Puppet cũ.

## Kiến thức cốt lõi

Một thư mục có `go.mod` là một module. `go run .` biên dịch rồi chạy package `main` trong thư mục đó. `go build -o check` để lại file `check`. Máy chạy file đó không cần cài `go`, miễn là cùng OS và kiến trúc CPU.

`flag.String` trả về một con trỏ. Giá trị chỉ đúng sau khi gọi `flag.Parse()`; trước đó nó vẫn là default.

`http.Get` dùng client mặc định, và client này không có timeout tổng. Request treo thì tool treo theo. Hãy tạo `http.Client{Timeout: 10 * time.Second}`; timeout này tính cả lúc kết nối lẫn lúc đọc body. Đọc xong phải `Close` body, nếu không connection sẽ không được tái sử dụng. Lab này thoát ngay nên việc đóng chủ yếu để giữ thói quen đúng. `io.Copy(io.Discard, resp.Body)` đọc hết body trước khi đóng để connection quay về pool. Với process sắp thoát thì bước này không đổi kết quả, nhưng đó là cách client được thiết kế để dùng.

`exec.Command("git", "status")` không đi qua shell, cùng lý do với list trong Python và `execFile` trong Node. `Run` trả `nil` khi exit code là 0. Lệnh chạy nhưng exit code khác 0 thì lỗi có kiểu `*exec.ExitError`, và `ExitCode()` là mã đó. Lỗi không phải `ExitError` nghĩa là lệnh không start được, ví dụ không có binary trong `PATH`.

## Lệnh và code mẫu

```bash
mkdir -p "$HOME/devops-lab/programming/gocheck"
cd "$HOME/devops-lab/programming/gocheck"
go mod init example.com/gocheck
```

`main.go`:

```go
package main

import (
	"flag"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"time"
)

func main() {
	url := flag.String("url", "https://example.com", "URL to check")
	flag.Parse()

	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Get(*url)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	defer resp.Body.Close()
	io.Copy(io.Discard, resp.Body)
	fmt.Println(resp.StatusCode)
	if resp.StatusCode >= 400 {
		os.Exit(1)
	}

	git := exec.Command("git", "--version")
	git.Stdout = os.Stdout
	git.Stderr = os.Stderr
	if err := git.Run(); err != nil {
		if exitErr, ok := err.(*exec.ExitError); ok {
			os.Exit(exitErr.ExitCode())
		}
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}
```

Build một file:

```bash
go build -o check .
./check --url https://example.com
```

## Thực hành

1. Chạy `go mod init` và lưu `main.go`.
2. `go run . --help`. `flag` in default của `--url`.
3. `go run .` với URL mặc định. Ghi lại status và dòng `git version`.
4. `go run . --url https://example.com/does-not-exist`. example.com trả 404. Process thoát trước khi chạy git, exit code 1.
5. `go build -o check .` rồi chạy `file check`, hoặc chỉ cần nhìn kích thước. Bạn sẽ thấy đó là một executable, không phải script.
6. Thử đổi sang gọi `http.Get` trực tiếp, tức là bỏ timeout, rồi đổi lại về `Client`.

## Kết quả mong đợi

- `--help` liệt kê `-url` và default `https://example.com`.
- URL 200: in `200`, rồi một dòng git version, exit code 0.
- URL 404: in `404`, exit code 1, không có dòng git version.
- File `check` chạy được bằng `./check` mà không gõ `go run`.

## Checklist

- [ ] Tạo module bằng `go mod init` trước khi build.
- [ ] Đọc flag sau `flag.Parse`.
- [ ] Đặt `Timeout` trên `http.Client`.
- [ ] Đóng response body.
- [ ] Phân biệt `ExitError` với lỗi không start được lệnh.
- [ ] Nói được khi nào đưa binary Go cho máy không có runtime, khi nào để script Python hoặc Node.

## Lỗi thường gặp

**Dùng `http.Get` cho health check production.** Vì không có timeout, một upstream treo sẽ kéo tool treo theo, và alert không bao giờ được gửi. Dùng `Client.Timeout`.

**Quên dereference flag pointer.** `url` có kiểu `*string`, nên in `url` ra bạn sẽ thấy địa chỉ bộ nhớ chứ không phải URL. Dùng `*url`.

**`exec.Command("sh", "-c", userInput)`.** Cách này lại đưa input qua shell. Hãy truyền executable và argument riêng.

**Commit file `check` nặng vài MB.** Binary build lại được từ source, nên hãy đưa nó vào `.gitignore` hoặc gắn vào release như bài tag đã hướng dẫn. Đừng để mỗi lần sửa một dòng code lại thêm vài MB vào history.

## Nguồn

1. [Tutorial: Getting started](https://go.dev/doc/tutorial/getting-started) — Go, module và `go run`.
2. [flag](https://pkg.go.dev/flag) — `String`, `Parse`, default và usage.
3. [os/exec](https://pkg.go.dev/os/exec) — `Command`, `Run`, `ExitError`.
4. [net/http](https://pkg.go.dev/net/http) — `Client.Timeout` và `Get`.

## Tiếp theo

[Ruby cho script vận hành](./04-ruby.md)
