Mỗi tệp Go thuộc về một package, được khai báo ở dòng đầu tiên. Code ở package này dùng package khác bằng cách import nó và thêm tên package làm tiền tố trước các tên. Chỉ những tên bắt đầu bằng chữ hoa mới dùng được từ bên ngoài package.
fmt, math và strings là các package của thư viện chuẩn. Chúng đi kèm Go, nên "thêm" một package nghĩa là viết tên nó vào khối import. Không có gì phải tải về. fmt.Println, math.Sqrt và strings.Repeat viết hoa vì chúng được export.
Mệnh đề package
Dòng đầu tiên không phải chú thích của mọi tệp .go là package name. Mọi tệp trong một thư mục phải dùng cùng một tên, và gộp lại chúng tạo thành một package. Thư mục có hai tên package khác nhau sẽ không biên dịch được:
found packages greeting (b.go) and greet (greet.go) in /home/ana/shop/greet
(Ngoại lệ duy nhất là các tệp test, chúng có thể dùng package name_test.)
package main là trường hợp đặc biệt: nó tạo ra một chương trình chạy được, và chương trình bắt đầu từ func main() của nó. Mọi package khác là thư viện, dùng để import.
Theo quy ước, tên package trùng với phần cuối của đường dẫn thư mục: code trong shop/greet là package greet. Tên package ngắn, viết thường, chỉ một từ, không có dấu gạch dưới hay chữ hoa xen giữa: strconv, httptest, greet, chứ không phải string_utils hay greetHelpers.
Import package
Đường dẫn import là một chuỗi. Với thư viện chuẩn, đó là đường dẫn của package trong cây mã nguồn của Go; với mọi thứ khác, nó bắt đầu bằng đường dẫn module:
import (
"fmt" // standard library
"net/http" // standard library, nested path
"math/rand/v2" // standard library, version 2 of math/rand
"github.com/google/uuid" // another module (needs go get)
"example.com/shop/greet" // a package in your own module
)
Đường dẫn là nơi package nằm; tên là thứ bạn gõ trong code. Với net/http, tên là http; với math/rand/v2, tên là rand. Bạn viết http.Get, không bao giờ viết net/http.Get.
gofmt và goimports sắp xếp khối import và theo quy ước tách các import của thư viện chuẩn khỏi phần còn lại bằng một dòng trống.
Import không dùng đến là lỗi biên dịch:
./main.go:5:2: "os" imported and not used
Các trình soạn thảo hỗ trợ Go chạy goimports mỗi khi lưu, công cụ này tự thêm các import chuẩn còn thiếu và xóa những import không dùng.
Tên exported và unexported
Khả năng nhìn thấy trong Go được quyết định bởi một quy tắc duy nhất: tên bắt đầu bằng chữ hoa thì được export. Quy tắc này áp dụng cho hàm, kiểu, biến, hằng số, trường của struct và phương thức. Go không có từ khóa public, private hay protected.
| Tên | Nhìn thấy được từ ngoài package? |
|---|---|
func Hello() | có |
func hello() | không |
type User struct | có |
trường User.Name | có |
trường User.email | không |
const MaxSize | có |
var defaultTimeout | không |
Dùng một tên unexported từ package khác sẽ gặp lỗi undefined:
./main.go:11:20: undefined: greet.prefix
Quy tắc này quan trọng ngay cả trong một tệp duy nhất, vì các package khác cũng xem xét kiểu của bạn. encoding/json chỉ nhìn thấy các trường exported:
token bị bỏ khỏi JSON một cách âm thầm. Đó là tính năng khi trường chứa dữ liệu riêng tư, và là bất ngờ khi bạn quên viết hoa một trường mà bạn muốn có.
Chia một package ra nhiều tệp
Một package có thể trải ra bao nhiêu tệp tùy ý. Mọi tệp trong thư mục có cùng dòng package đều nhìn thấy mọi tên được khai báo ở các tệp khác, dù exported hay không, mà không cần import gì:
shop/
├── go.mod (module example.com/shop)
├── main.go
└── math.go
// main.go
package main
import "fmt"
func main() {
fmt.Println(total(2, 3))
}
// math.go
package main
func total(a, b int) int { return a + b }
main.go gọi trực tiếp total. Giữa các tệp của cùng một package không có #include và cũng không có import.
Lỗi "undefined" khi chạy go run main.go
Đây là vấn đề về package được tìm kiếm nhiều nhất trong Go, và nó chỉ có một nguyên nhân. Chạy riêng tệp chứa main thì chỉ tệp đó được biên dịch:
go run main.go
# command-line-arguments
./main.go:6:14: undefined: total
command-line-arguments là tên Go đặt cho package được build từ một danh sách tệp, và math.go không có trong danh sách đó. Hãy chạy cả package:
go run .
5
Điều tương tự áp dụng cho go build main.go. Dùng go run . và go build (hoặc go build ./cmd/app) thì vấn đề này không bao giờ xuất hiện. Nếu vẫn gặp lỗi với go run ., hãy kiểm tra xem cả hai tệp có cùng ghi package main không và có tệp nào bị loại ra bởi build constraint (//go:build) hay hậu tố _test.go không.
Package của riêng bạn
Một thư mục con trong module của bạn là một package riêng. Import nó bằng đường dẫn module trong go.mod cộng với thư mục:
shop/
├── go.mod (module example.com/shop)
├── main.go
└── greet/
└── greet.go
// greet/greet.go
package greet
const prefix = "Hi, "
// Hello returns a greeting for name.
func Hello(name string) string {
return prefix + name
}
// main.go
package main
import (
"fmt"
"example.com/shop/greet"
)
func main() {
fmt.Println(greet.Hello("Ana"))
}
Hi, Ana
Có hai chỗ hay làm người mới vấp. Đường dẫn import là đường dẫn module cộng với thư mục, không phải đường dẫn tương đối: "./greet" không chạy ở chế độ module. Và prefix bị ẩn khỏi main vì nó viết thường; chỉ Hello là một phần API của package.
Package internal
Thư mục tên internal giới hạn ai được import những gì nằm bên trong. Code trong shop/internal/store có thể được import bởi bất kỳ package nào có gốc là shop/, và không package nào bên ngoài. Một module khác cố import sẽ nhận được:
use of internal package example.com/shop/internal/store not allowed
Hãy dùng internal/ cho code bạn muốn chia sẻ giữa các package của mình mà không cam kết với phần còn lại của thế giới. Bạn có thể thay đổi API của nó thoải mái, vì không code bên ngoài nào phụ thuộc vào nó được.
Alias khi import
Đặt cho một import tên cục bộ khác bằng cách viết tên đó trước đường dẫn. Lý do thường gặp là hai package trùng tên:
Cả hai package đều tên rand, nên ít nhất một cái cần alias. Hãy dùng alias khi bị trùng tên hoặc khi tên không rõ nghĩa; đừng đổi tên package chỉ để viết cho ngắn, vì người đọc biết strconv chứ không biết sc của bạn.
Blank import và dot import
Blank import (_). Import một package chỉ để lấy tác dụng phụ: các biến cấp package và hàm init của nó được chạy, nhưng bạn không dùng tên nào của nó. Các driver cơ sở dữ liệu và bộ giải mã ảnh tự đăng ký theo cách này:
import (
"database/sql"
_ "github.com/lib/pq" // registers the "postgres" driver with database/sql
)
Nếu không có _, import này sẽ là lỗi import không dùng đến.
Dot import (.). Đưa các tên exported của package thẳng vào phạm vi của tệp, nên bạn viết Println thay vì fmt.Println. Cách này che mất nguồn gốc của các tên và không được khuyến khích, trừ vài mẫu hàm hỗ trợ cho test.
Import vòng tròn
Go không cho phép hai package import lẫn nhau, dù trực tiếp hay qua một chuỗi:
package example.com/shop
imports example.com/shop/greet from main.go
imports example.com/shop/other from c.go
imports example.com/shop/greet from o.go: import cycle not allowed
Thông báo lỗi liệt kê cả chuỗi. Hai cách sửa sau đây xử lý được gần như mọi trường hợp:
- Chuyển phần code mà cả hai package cần sang một package thứ ba ở tầng thấp hơn, để cả hai cùng import.
- Nếu package A chỉ cần gọi một thứ trong B, hãy định nghĩa một interface nhỏ trong A và để kiểu của B thỏa mãn nó. A không còn import B nữa.
Import vòng tròn thường có nghĩa là ranh giới package đang được vạch quanh những lớp code thực ra thuộc về nhau, nên đó cũng là tín hiệu để xem lại cách chia.
Lỗi thường gặp
- Chạy một tệp duy nhất.
go run main.gotrong package nhiều tệp: hãy dùnggo run .. - Import tương đối.
import "./utils"thất bại ở chế độ module. Hãy dùng"example.com/yourmodule/utils". - Viết thường tên bạn định export. Hàm, trường hay phương thức mà package khác cần dùng phải bắt đầu bằng chữ hoa.
- Tên package không khớp với thư mục. Hợp lệ nhưng gây rối: đường dẫn import ghi
utilscòn code lại ghihelpers.X. Hãy giữ chúng giống nhau. - Tên package "gom tất cả".
util,commonvàmisckhông nói gì về nội dung bên trong và dễ trở thành nơi hút import vòng tròn. Hãy đặt tên package theo thứ nó cung cấp:money,auth,slug.
Câu hỏi thường gặp
Vì sao Go báo một hàm trong cùng package là undefined?
Có lẽ bạn đã chạy go run main.go. Chỉ định tên tệp thì chỉ tệp đó được biên dịch, nên các hàm định nghĩa ở những tệp khác của cùng package bị thiếu: undefined: total. Hãy chạy go run ., lệnh này biên dịch mọi tệp .go trong thư mục. Đồng thời kiểm tra xem mọi tệp có cùng một dòng package không.
Làm sao thêm package fmt trong Go?
fmt thuộc thư viện chuẩn và đi kèm Go, nên không cần cài gì cả. Thêm import "fmt" sau dòng package và gọi các hàm của nó với tiền tố fmt., ví dụ fmt.Println("hi"). Điều tương tự áp dụng cho strings, os, time, net/http và mọi package chuẩn khác.
Làm sao import package của chính mình trong Go?
Import theo đường dẫn module cộng với thư mục. Trong một module tên example.com/shop, package nằm trong thư mục greet được import bằng import "example.com/shop/greet" và dùng như greet.Hello(). Chỉ những tên bắt đầu bằng chữ hoa mới nhìn thấy được từ nơi import.
Exported và unexported trong Go khác nhau thế nào?
Tên bắt đầu bằng chữ hoa (Hello, Total, User.Name) là exported và dùng được từ package khác. Tên viết thường (hello, total, user.name) là unexported và chỉ nhìn thấy bên trong package của nó. Go không có từ khóa public hay private.
Làm sao sửa lỗi "import cycle not allowed" trong Go?
Hai package import lẫn nhau, trực tiếp hoặc qua một chuỗi, và Go cấm điều này. Hãy chuyển phần code dùng chung sang một package thứ ba mà cả hai cùng import, hoặc định nghĩa một interface nhỏ trong package cần hành vi đó để nó không còn phải import package kia.