مدیریت و خودکارسازی شماره نسخهها
در همان ابتدای این کتاب، ما شماره نسخه برنامه خود را به صورت ثابت در مقدار ثابت "1.0.0" در فایل cmd/api/main.go تعریف کردیم.
در این فصل، اقداماتی انجام میدهیم تا مشاهده و مدیریت این شماره نسخه آسانتر شود، و همچنین توضیح میدهیم که چگونه میتوانید شماره نسخهها را به صورت خودکار بر اساس Git commits تولید کنید و آنها را در برنامه خود ادغام کنید.
نمایش شماره نسخه
بیایید با بهروزرسانی برنامه خود شروع کنیم تا بتوانیم به راحتی شماره نسخه را با اجرای binary با پرچم -version در خط فرمان بررسی کنیم، مشابه این:
$ ./bin/api -version Version: 1.0.0
از نظر مفهومی، پیادهسازی این قابلیت نسبتاً ساده است. ما باید یک پرچم version boolean در خط فرمان تعریف کنیم، وجود این پرچم را هنگام راهاندازی بررسی کنیم و سپس در صورت لزوم شماره نسخه را چاپ کرده و برنامه را ببندیم.
اگر میخواهید همراه با ما پیش بروید، فایل cmd/api/main.go خود را به صورت زیر بهروزرسانی کنید:
package main import ( "context" "database/sql" "expvar" "flag" "fmt" // New import "log/slog" "os" "runtime" "strings" "sync" "time" "greenlight.alexedwards.net/internal/data" "greenlight.alexedwards.net/internal/mailer" _ "github.com/lib/pq" ) const version = "1.0.0" ... func main() { var cfg config flag.IntVar(&cfg.port, "port", 4000, "API server port") flag.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)") flag.StringVar(&cfg.db.dsn, "db-dsn", "", "PostgreSQL DSN") flag.IntVar(&cfg.db.maxOpenConns, "db-max-open-conns", 25, "PostgreSQL max open connections") flag.IntVar(&cfg.db.maxIdleConns, "db-max-idle-conns", 25, "PostgreSQL max idle connections") flag.DurationVar(&cfg.db.maxIdleTime, "db-max-idle-time", 15*time.Minute, "PostgreSQL max connection idle time") flag.BoolVar(&cfg.limiter.enabled, "limiter-enabled", true, "Enable rate limiter") flag.Float64Var(&cfg.limiter.rps, "limiter-rps", 2, "Rate limiter maximum requests per second") flag.IntVar(&cfg.limiter.burst, "limiter-burst", 4, "Rate limiter maximum burst") flag.StringVar(&cfg.smtp.host, "smtp-host", "sandbox.smtp.mailtrap.io", "SMTP host") flag.IntVar(&cfg.smtp.port, "smtp-port", 25, "SMTP port") flag.StringVar(&cfg.smtp.username, "smtp-username", "a7420fc0883489", "SMTP username") flag.StringVar(&cfg.smtp.password, "smtp-password", "e75ffd0a3aa5ec", "SMTP password") flag.StringVar(&cfg.smtp.sender, "smtp-sender", "Greenlight <no-reply@greenlight.alexedwards.net>", "SMTP sender") flag.Func("cors-trusted-origins", "Trusted CORS origins (space separated)", func(val string) error { cfg.cors.trustedOrigins = strings.Fields(val) return nil }) // Create a new version boolean flag with the default value of false. displayVersion := flag.Bool("version", false, "Display version and exit") flag.Parse() // If the version flag value is true, then print out the version number and // immediately exit. if *displayVersion { fmt.Printf("Version:\t%s\n", version) os.Exit(0) } ... } ...
خب، بیایید این را امتحان کنیم. binaryها را با استفاده از make build/api مجدداً بسازید، سپس binary ./bin/api را با پرچم -version اجرا کنید.
باید مشاهده کنید که شماره نسخه را چاپ میکند و سپس خارج میشود، مشابه این:
$ make build/api Building cmd/api... go build -ldflags="-s" -o="./bin/api" ./cmd/api GOOS=linux GOARCH=amd64 go build -ldflags="-s" -o="./bin/linux_amd64/api" ./cmd/api $ ./bin/api -version Version: 1.0.0
شمارهگذاری خودکار نسخه با Git
Go اطلاعات version control را در binaryهای شما هنگام اجرای go build روی یک package main که با Git، Mercurial، Fossil یا Bazaar ردیابی میشود، قرار میدهد.
دو روش برای دسترسی به این اطلاعات version control وجود دارد — یا با استفاده از دستور go version -m روی binary خود، یا از داخل کد برنامه خود با فراخوانی debug.ReadBuildInfo().
بیایید هر دو رویکرد را بررسی کنیم.
اگر میخواهید همراه با ما پیش بروید (و قبلاً این کار را انجام ندادهاید)، لطفاً یک مخزن Git جدید در ریشه پروژه خود مقداردهی اولیه کنید:
$ git init Initialized empty Git repository in /home/alex/Projects/greenlight/.git/
سپس یک commit جدید حاوی تمام فایلهای پروژه خود ایجاد کنید، به صورت زیر:
$ git add . $ git commit -m "Initial commit"
اگر تاریخچه commit خود را با استفاده از دستور git log مشاهده کنید، hash این commit را خواهید دید.
$ git log commit 59bdb76fda0c15194ce18afae5d4875237f05ea9 (HEAD -> master) Author: Alex EdwardsDate: Wed Feb 19 19:01:34 2025 +0100 Initial commit
در مورد من، hash commit 59bdb76fda0c15194ce18afae5d4875237f05ea9 است — اما مال شما بسیار محتمل است که مقدار متفاوتی باشد.
بعد، دوباره make build را اجرا کنید تا binary جدیدی تولید شود و سپس از دستور go version -m روی آن استفاده کنید. به صورت زیر:
$ make build/api
Building cmd/api...
go build -ldflags="-s" -o=./bin/api ./cmd/api
GOOS=linux GOARCH=amd64 go build -ldflags="-s" -o=./bin/linux_amd64/api ./cmd/api
$ go version -m ./bin/api
./bin/api: go1.25.0
path greenlight.alexedwards.net/cmd/api
mod greenlight.alexedwards.net v0.0.0-20250219190134-59bdb76fda0c
dep github.com/julienschmidt/httprouter v1.3.0
dep github.com/lib/pq v1.10.9
dep github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
dep github.com/wneessen/go-mail v0.6.2
dep golang.org/x/crypto v0.41.0
dep golang.org/x/text v0.22.0
dep golang.org/x/time v0.12.0
build -buildmode=exe
build -compiler=gc
build -ldflags=-s
build CGO_ENABLED=1
build CGO_CFLAGS=
build CGO_CPPFLAGS=
build CGO_CXXFLAGS=
build CGO_LDFLAGS=
build GOARCH=amd64
build GOOS=linux
build GOAMD64=v1
build vcs=git
build vcs.revision=59bdb76fda0c15194ce18afae5d4875237f05ea9
build vcs.time=2025-02-19T19:01:34Z
build vcs.modified=false
خروجی go version -m اطلاعات جالبی درباره binary به ما نشان میدهد. میتوانیم نسخه Go که با آن ساخته شده را ببینیم (go1.25.0 در مورد من)، وابستگیهای module و اطلاعات مربوط به تنظیمات build — از جمله پرچمهای linker استفاده شده و OS و معماری که برای آن ساخته شده است.
با این حال، چیزهایی که در حال حاضر بیشتر به آنها علاقهمندیم خط mod و تنظیمات build vcs در انتهای خروجی هستند.
vcs=gitبه ما میگوید که سیستم version control مورد استفاده Git است.vcs.revisionhash آخرین Git commit است.vcs.timeزمانی است که این commit ایجاد شده است.vcs.modifiedبه ما میگوید که آیا کد ردیابی شده توسط مخزن Git از زمان ایجاد commit تغییر کرده است یا خیر. مقدارfalseنشان میدهد که کد تغییر نکرده است، به این معنی که binary با دقیقاً کد از commitvcs.revisionساخته شده است. مقدارtrueنشان میدهد که مخزن version control هنگام ساخت binary ‘dirty’ بوده است — و ممکن است کد مورد استفاده برای ساخت binary دقیقاً کد از commitvcs.revisionنباشد.
خط mod greenlight.alexedwards.net v0.0.0-20250219190134-59bdb76fda0c به ما میگوید که module اصلی در binary کامپایل شده greenlight.alexedwards.net است و module دارای نسخه v0.0.0-20250219190134-59bdb76fda0c است.
همانطور که به طور خلاصه در بالا ذکر کردم، تمام اطلاعاتی که در خروجی go version -m میبینید در زمان runtime نیز برای شما در دسترس است. به طور دقیقتر، میتوانید با فراخوانی تابع debug.ReadBuildInfo() به آنها دسترسی پیدا کنید که یک struct debug.BuildInfo برمیگرداند که در اصل حاوی همان اطلاعاتی است که هنگام اجرای دستور go version -m دیدیم.
بیایید از این قابلیت استفاده کنیم و فایل main.go خود را طوری تطبیق دهیم که مقدار version به جای مقدار ثابت hardcoded "1.0.0"، به pseudo-version که درباره آن بحث کردیم تنظیم شود.
برای کمک به این کار، یک package کوچک internal/vcs شامل یک تابع Version() ایجاد میکنیم، به صورت زیر:
$ mkdir internal/vcs $ touch internal/vcs/vcs.go
package vcs import ( "fmt" "runtime/debug" ) func Version() string { // Use debug.ReadBuildInfo() to retrieve a debug.BuildInfo struct. If this is available, // the ok value will be true, and we return the pseudo-version contained in the // Main.Version field. bi, ok := debug.ReadBuildInfo() if ok { return bi.Main.Version } return "" }
حالا که این آماده است، بیایید به فایل main.go خود برگردیم و آن را طوری بهروزرسانی کنیم که شماره نسخه با استفاده از تابع جدید vcs.Version() تنظیم شود:
package main import ( "context" "database/sql" "expvar" "flag" "fmt" "log/slog" "os" "runtime" "strings" "sync" "time" "greenlight.alexedwards.net/internal/data" "greenlight.alexedwards.net/internal/mailer" "greenlight.alexedwards.net/internal/vcs" // New import _ "github.com/lib/pq" ) // Make version a variable (rather than a constant) and set its value to vcs.Version(). var ( version = vcs.Version() ) ...
خب، بیایید این را امتحان کنیم. binary را دوباره بسازید…
$ make build/api Building cmd/api... go build -ldflags="-s" -o=./bin/api ./cmd/api GOOS=linux GOARCH=amd64 go build -ldflags="-s" -o=./bin/linux_amd64/api ./cmd/api
و سپس آن را با پرچم -version اجرا کنید:
$ ./bin/api -version Version: v0.0.0-20250219190134-59bdb76fda0c+dirty
عالی! اکنون شماره pseudo-version موجود در binary را گزارش میدهد. اما از آنجا که از زمان commit قبلی کد را تغییر دادهایم، pseudo-version اکنون پسوند +dirty دارد که نشان میدهد کدپایه تغییرات commit نشده دارد.
بیایید با commit کردن تغییرات اخیر خود این مشکل را برطرف کنیم…
$ git add . $ git commit -m "Generate version number automatically"
و هنگامی که binary را مجدداً بسازید و شماره نسخه را دوباره بررسی کنید، باید شماره نسخه جدیدی بدون پسوند +dirty ببینید، مشابه این:
$ make build/api Building cmd/api... go build -ldflags="-s" -o=./bin/api ./cmd/api GOOS=linux GOARCH=amd64 go build -ldflags="-s" -o=./bin/linux_amd64/api ./cmd/api $ ./bin/api -version Version: v0.0.0-20250221115919-f79a5dbadf36
برچسب زدن releases
در برخی پروژهها ممکن است بخواهید برخی Git commits را با یک شماره نسخه semantic برچسب درج کنید، که معمولاً برای نشان دادن یک release رسمی است.
برای نشان دادن این مورد، بیایید برچسب v1.0.0 را به آخرین commit خود اضافه کنیم به صورت زیر:
$ git tag v1.0.0 $ git log commit f79a5dbadf3665b825aef58b27406920ac6382d7 (HEAD -> master, tag: v1.0.0) Author: Alex EdwardsDate: Fri Feb 21 12:59:19 2025 +0100 Generate version number automatically ...
اگر برنامه را مجدداً بسازید و نسخه را دوباره بررسی کنید، باید مشاهده کنید که اکنون v1.0.0 به عنوان نسخه گزارش میشود، به جای pseudo-version تولید شده خودکار.
$ make build/api $ ./bin/api -version Version: v1.0.0
اکنون، فقط برای نشان دادن اینکه بعد چه اتفاقی میافتد، بیایید یک commit (خالی) دیگر به کدپایه خود اضافه کنیم.
$ git commit --allow-empty -m "Empty commit" [master 5370944] Empty commit
در این مرحله، نسخه پایه ما هنوز v1.0.0 است، اما ما جلوتر از commit هستیم که آن را v1.0.0 برچسب زدیم. بنابراین اکنون Go دیگر v1.0.0 را به عنوان نسخه گزارش نمیکند و در عوض دوباره یک pseudo-version گزارش میدهد.
هنگامی که برنامه را مجدداً بسازید و آن را بررسی کنید، باید مشاهده کنید که نسخه چیزی شبیه به این است:
$ make build/api $ ./bin/api -version Version: v1.0.1-0.20250221122119-5370944738f9
timestamp و hash مربوط به آخرین commit هستند، درست مثل قبل. اما همچنین میتوانیم ببینیم که بخش شماره نسخه semantic v1.0.1-0 است، که در نگاه اول کمی عجیب به نظر میرسد.
آنچه در اینجا اتفاق میافتد این است که Go قاعده زیر را اعمال میکند: اگر نسخه پایه vX.Y.Z دارید، اما آخرین commits شما جلوتر از آن هستند، پس ابتدای pseudo-version به فرمت vX.Y.(Z+1)-0 خواهد بود. برای اطلاعات بیشتر درباره دلیل این موضوع، لطفاً به Go Modules Reference مراجعه کنید.
پس در مجموع، این بسیار خوب است. ما شماره نسخهای داریم که به صورت خودکار بر اساس تاریخچه Git ما تولید شده و در binary ما قرار گرفته است، و شناسایی اینکه دقیقاً چه کدی در یک binary خاص یا یک برنامه در حال اجرا استفاده شده آسان است — فقط کافی است binary را با پرچم -version اجرا کنید، یا endpoint healthcheck را فراخوانی کنید و سپس شماره نسخه را با تاریخچه مخزن Git مقایسه کنید.
اطلاعات تکمیلی
گزارش فقط timestamp و hash commit
اگر commits را برچسب نمیزنید و میخواهید فقط timestamp و hash آخرین commit را به عنوان شماره نسخه خود استفاده کنید، میتوانید در فیلد debug.BuildInfo.Settings حلقه بزنید تا مقادیر vcs.time، vcs.revision و vcs.modified را استخراج کنید و رشته نسخه خود را ایجاد کنید. به صورت زیر:
func Version() string { var ( time string revision string modified bool ) bi, ok := debug.ReadBuildInfo() if ok { for _, s := range bi.Settings { switch s.Key { case "vcs.time": time = s.Value case "vcs.revision": revision = s.Value case "vcs.modified": if s.Value == "true" { modified = true } } } } if modified { return fmt.Sprintf("%s-%s+dirty", time, revision) } return fmt.Sprintf("%s-%s", time, revision) }
استفاده از این روش شماره نسخههایی مشابه این ایجاد میکند:
2025-02-21T10:16:24Z-1c9b6ff48ea800acdf4f5c6f5c3b62b98baf2bd7+dirty
استفاده از پرچمهای linker
قبل از Go 1.18، روش رایج برای مدیریت خودکار شماره نسخهها، ‘ burns-in’ کردن شماره نسخه هنگام ساخت binary با استفاده از پرچم linker -X بود. استفاده از debug.ReadBuildInfo() اکنون روش ترجیحی است، اما رویکرد قدیمی هنوز میتواند مفید باشد اگر بخواهید شماره نسخه را روی چیزی تنظیم کنید که از طریق debug.ReadBuildInfo() در دسترس نیست.
به عنوان مثال، اگر بخواهید شماره نسخه را روی مقدار متغیر محیطی VERSION در ماشینی که binary را میسازد تنظیم کنید، میتوانید از پرچم linker -X برای ‘ burns-in’ کردن این مقدار به متغیر main.version استفاده کنید. به صورت زیر:
... ## build/api: build the cmd/api application .PHONY: build/api build/api: @echo 'Building cmd/api...' go build -ldflags='-s -X main.version=${VERSION}' -o=./bin/api ./cmd/api GOOS=linux GOARCH=amd64 go build -ldflags='-s -X main.version=${VERSION}' -o=./bin/linux_amd64/api ./cmd/api