پروکسیهای ماژول و vendoring
یکی از خطرات استفاده از پکیجهای شخص ثالث در کد Go شما این است که ممکن است مخزن پکیج دیگر در دسترس نباشد. به عنوان مثال، پکیج httprouter نقش مهمی در برنامه ما ایفا میکند و اگر نویسنده آن تصمیم بگیرد آن را از GitHub حذف کند، ما با دردسر بزرگی مواجه خواهیم شد تا آن را با یک جایگزین عوض کنیم.
(من قصد ندارم بگویم این اتفاق به احتمال زیاد با httprouter رخ میدهد — فقط آن را به عنوان مثال استفاده میکنم!)
خوشبختانه، Go دو راه برای کاهش این خطر ارائه میدهد: پروکسیهای ماژول و vendoring.
پروکسیهای ماژول
Go به طور پیشفرض از پروکسیهای ماژول (همچنین به عنوان آینههای ماژول شناخته میشوند) پشتیبانی میکند. اینها سرویسهایی هستند که کد منبع را از مخازن اصلی و معتبر مانند GitHub، GitLab یا BitBucket کپی میکنند.
دستور go env را روی ماشین خود اجرا کنید تا تنظیمات محیط عملیاتی Go خود را چاپ کند. خروجی شما باید شبیه به این باشد:
$ go env AR='ar' CC='gcc' CGO_CFLAGS='-O2 -g' CGO_CPPFLAGS='' CGO_CXXFLAGS='-O2 -g' CGO_ENABLED='1' CGO_FFLAGS='-O2 -g' CGO_LDFLAGS='-O2 -g' CXX='g++' GCCGO='gccgo' GO111MODULE='' GOAMD64='v1' GOARCH='amd64' GOAUTH='netrc' GOBIN='' GOCACHE='/home/alex/.cache/go-build' GOCACHEPROG='' GODEBUG='' GOENV='/home/alex/.config/go/env' GOEXE='' GOEXPERIMENT='' GOFIPS140='off' GOFLAGS='' GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build1737714937=/tmp/go-build -gno-record-gcc-switches' GOHOSTARCH='amd64' GOHOSTOS='linux' GOINSECURE='' GOMOD='/home/alex/Projects/greenlight/go.mod' GOMODCACHE='/home/alex/go/pkg/mod' GONOPROXY='' GONOSUMDB='' GOOS='linux' GOPATH='/home/alex/go' GOPRIVATE='' GOPROXY='https://proxy.golang.org,direct' GOROOT='/usr/local/go' GOSUMDB='sum.golang.org' GOTELEMETRY='on' GOTELEMETRYDIR='/home/alex/.config/go/telemetry' GOTMPDIR='' GOTOOLCHAIN='auto' GOTOOLDIR='/usr/local/go/pkg/tool/linux_amd64' GOVCS='' GOVERSION='go1.25.0' GOWORK='' PKG_CONFIG='pkg-config'
نکته مهمی که باید به آن توجه کنید تنظیم GOPROXY است که شامل لیستی از آینههای ماژول جدا شده با کاما است. به طور پیشفرض مقدار زیر را دارد:
GOPROXY="https://proxy.golang.org,direct"
آدرس https://proxy.golang.org که در اینجا میبینیم به آینه ماژولی اشاره میکند که توسط تیم Go در Google نگهداری میشود و حاوی نسخههایی از کد منبع هزاران پکیج open-source Go است.
هر زمان که یک پکیج را با استفاده از دستور go — چه با go get و چه با یکی از دستورات go mod * — دریافت میکنید، ابتدا سعی میکند کد منبع را از این آینه بازیابی کند.
اگر آینه قبلاً کپی ذخیره شدهای از کد منبع برای پکیج و شماره نسخه مورد نیاز داشته باشد، بلافاصله این کد را در یک فایل zip برمیگرداند. در غیر این صورت، اگر ذخیره نشده باشد، آینه سعی میکند کد را از مخزن اصلی دریافت کند، آن را به شما ارسال کند و برای استفاده آینده ذخیره کند.
اگر آینه نتواند کد را دریافت کند، یک پاسخ خطا برمیگرداند و ابزار go به دریافت مستقیم کپی از مخزن اصلی بازمیگردد (به لطف دستور direct در تنظیم GOPROXY).
استفاده از آینه ماژول به عنوان محل دریافت اولیه چندین مزیت دارد:
- آینه ماژول
https://proxy.golang.orgمعمولاً پکیجها را برای مدت طولانی ذخیره میکند و در نتیجه در صورت ناپدید شدن مخزن اصلی از اینترنت، درجهای از محافظت را فراهم میکند. - غیرممکن است که پس از ذخیره شدن در آینه ماژول
https://proxy.golang.org، پکیجی را بازنویسی یا حذف کنید. این میتواند از ایجاد باگها یا مشکلاتی که ممکن است در صورت انتشار نسخه ویرایش شده پکیج با شماره نسخه یکسان توسط نویسنده پکیج (یا یک مهاجم) رخ دهد، جلوگیری کند. - دریافت ماژولها از آینه
https://proxy.golang.orgمیتواند بسیار سریعتر از دریافت آنها از مخازن اصلی باشد.
در بیشتر موارد، به طور کلی پیشنهاد میکنم تنظیم GOPROXY را با مقدار پیشفرض آن رها کنید.
اما اگر نمیخواهید از آینه ماژول ارائه شده توسط Google استفاده کنید، یا پشت فایروالی هستید که آن را مسدود میکند، جایگزینهای دیگری مانند https://goproxy.io و https://athens.azurefd.net ارائه شده توسط Microsoft وجود دارد که میتوانید به جای آن امتحان کنید. یا حتی میتوانید با استفاده از پروژههای open-source Athens و goproxy آینه ماژول خود را میزبانی کنید.
به عنوان مثال، اگر میخواهید به استفاده از https://goproxy.io به عنوان آینه اصلی و https://proxy.golang.org به عنوان آینه ثانویه قبل از بازگشت به دریافت مستقیم تغییر دهید، میتوانید تنظیم GOPROXY خود را به این صورت بهروز کنید:
$ export GOPROXY=https://goproxy.io,https://proxy.golang.org,direct
یا اگر میخواهید آینههای ماژول را به طور کلی غیرفعال کنید، میتوانید مقدار آن را به سادگی روی direct تنظیم کنید:
$ export GOPROXY=direct
Vendoring
قابلیت آینه ماژول Go عالی است و من استفاده از آن را توصیه میکنم. اما این یک راه حل جادویی برای همه توسعهدهندگان و همه پروژهها نیست.
به عنوان مثال، شاید نخواهید از آینه ماژول ارائه شده توسط Google یا یک شخص ثالث دیگر استفاده کنید، اما همچنین نمیخواهید هزینه میزبانی آینه خود را بپردازید. یا شاید نیاز دارید به طور منظم در محیطی بدون دسترسی به شبکه کار کنید. در این سناریوها احتمالاً همچنان میخواهید خطر یک وابستگی ناپدید شده را کاهش دهید، اما استفاده از آینه ماژول ممکن یا جذاب نیست.
همچنین باید بدانید که آینه ماژول پیشفرض proxy.golang.org تضمین نمیکند که کپی ماژول را برای همیشه ذخیره کند. از سوالات متداول:
proxy.golang.org همه ماژولها را برای همیشه ذخیره نمیکند. دلایل متعددی برای این امر وجود دارد، اما یکی از دلایل این است که اگر proxy.golang.org نتواند مجوز مناسبی را تشخیص دهد. در این صورت، فقط یک کپی کش شده موقت از ماژول در دسترس خواهد بود و ممکن است در صورت حذف از منبع اصلی و قدیمی شدن، غیرقابل دسترس شود.
علاوه بر این، اگر نیاز دارید پس از ۵ یا ۱۰ سال به یک کد سرد بازگردید، آیا آینه ماژول proxy.golang.org همچنان در دسترس خواهد بود؟ امیدواریم که باشد — اما نمیتوان با اطمینان گفت.
بنابراین، به این دلایل، میتواند منطقی باشد که وابستگیهای پروژه خود را با استفاده از دستور go mod vendor وندور کنید. وندور کردن وابستگیها به این صورت در اساس کپی کاملی از کد منبع پکیجهای شخص ثالث را در پوشه vendor پروژه شما ذخیره میکند.
بیایید نحوه انجام این کار را نشان دهیم. با تطبیق قاعده make tidy خود برای فراخوانی دستورات go mod verify و go mod vendor شروع میکنیم، به این صورت:
... # ==================================================================================== # # QUALITY CONTROL # ==================================================================================== # ## tidy: tidy and vendor module dependencies, and format all .go files .PHONY: tidy tidy: @echo 'Tidying module dependencies...' go mod tidy @echo 'Verifying and vendoring module dependencies...' go mod verify go mod vendor @echo 'Formatting .go files...' go fmt ./... ...
برای روشن شدن آنچه در پشت صحنه اتفاق میافتد، بیایید به سرعت بررسی کنیم چه اتفاقی میافتد وقتی make tidy را اجرا میکنیم:
- دستور
go mod tidyمطمئن میشود که فایلهایgo.modوgo.sumتمام وابستگیهای ضروری پروژه ما را فهرست کنند (و هیچ وابستگی غیرضروریای نداشته باشند). - دستور
go mod verifyبررسی میکند که وابستگیهای ذخیره شده در کش ماژول شما (واقع شده در ماشین شما در$GOPATH/pkg/mod) با checksumهای موجود در فایلgo.sumمطابقت داشته باشند. - سپس دستور
go mod vendorکد منبع ضروری را از کش ماژول شما به پوشه جدیدvendorدر ریشه پروژه شما کپی میکند.
بیایید این را امتحان کنیم و قاعده جدید tidy را به این صورت اجرا کنیم:
$ make tidy Tidying module dependencies... go mod tidy Verifying and vendoring module dependencies... go mod verify all modules verified go mod vendor Formatting .go files... go fmt ./...
پس از تکمیل این کار، باید ببینید که یک پوشه جدید vendor ایجاد شده که حاوی کپیهایی از تمام کد منبع به همراه یک فایل modules.txt است. ساختار دایرکتوری در پوشه vendor شما باید شبیه به این باشد:
$ tree -L 3 ./vendor/ ./vendor/ ├── github.com │ ├── BurntSushi │ │ └── toml │ ├── julienschmidt │ │ └── httprouter │ ├── lib │ │ └── pq │ ├── tomasen │ │ └── realip │ └── wneessen │ └── go-mail ├── golang.org │ └── x │ ├── crypto │ ├── exp │ ├── mod │ ├── sync │ ├── text │ ├── time │ └── tools ├── honnef.co │ └── go │ └── tools └── modules.txt
اکنون، وقتی دستوری مانند go run، go test یا go build را اجرا میکنید، ابزار go وجود پوشه vendor را تشخیص میدهد و کد وابستگی در پوشه vendor استفاده خواهد شد — به جای کد در کش ماژول ماشین محلی شما.
اگر مایلید، بیایید اجرای برنامه API را امتحان کنید. باید ببینید که همه چیز کامپایل میشود و دقیقاً مانند قبل کار میکند.
$ make run/api go run ./cmd/api -db-dsn=postgres://greenlight:pa55word@localhost/greenlight time=2023-09-10T10:59:13.722+02:00 level=INFO msg="database connection pool established" time=2023-09-10T10:59:13.722+02:00 level=INFO msg="starting server" addr=:4000 env=development
از آنجایی که تمام کد وابستگی اکنون در خود مخزن پروژه شما ذخیره شده است، میتوانید آن را همراه با بقیه کد خود در Git (یا یک سیستم کنترل نسخه جایگزین) ثبت کنید. این موضوع آرامشبخش است زیرا به شما مالکیت تمام کدهای مورد استفاده برای ساخت و اجرای برنامههای خود را میدهد، تحت کنترل نسخه نگهداری میشود.
عیب این کار، البته، این است که اندازه و حجم مخزن پروژه شما را افزایش میدهد. این نگرانی به ویژه در پروژههایی که وابستگیهای زیادی دارند و مخزن بسیار کلون خواهد شد، مانند پروژههایی که سیستم CI/CD با هر commit جدید مخزن را کلون میکند، مطرح است.
بیایید همچنین نگاهی سریع به فایل vendor/modules.txt ایجاد شده بیندازیم. اگر از مراحل پیروی کردهاید، باید شبیه به این باشد:
# github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c ## explicit; go 1.18 github.com/BurntSushi/toml github.com/BurntSushi/toml/internal # github.com/julienschmidt/httprouter v1.3.0 ## explicit; go 1.7 github.com/julienschmidt/httprouter # github.com/lib/pq v1.10.9 ## explicit; go 1.13 github.com/lib/pq github.com/lib/pq/oid github.com/lib/pq/scram # github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce ## explicit github.com/tomasen/realip # github.com/wneessen/go-mail v0.6.2 ## explicit; go 1.16 github.com/wneessen/go-mail github.com/wneessen/go-mail/internal/pkcs7 github.com/wneessen/go-mail/log ... etc.
فایل vendor/modules.txt اساساً فهرستی از پکیجهای vendored شده و شماره نسخههای آنهاست. وقتی vendoring استفاده میشود، ابزار go بررسی میکند که شماره نسخههای ماژول در modules.txt با شماره نسخههای موجود در فایل go.mod مطابقت داشته باشند. اگر عدم انطباقی وجود داشته باشد، ابزار go خطا گزارش میکند.
در نهایت، باید از ایجاد هرگونه تغییر در کد موجود در دایرکتوری vendor خودداری کنید. انجام این کار میتواند بالقوه باعث سردرگمی شود (زیرا کد دیگر با نسخه اصلی کد منبع مطابقت نخواهد داشت) و — علاوه بر این — اجرای go mod vendor هر بار که آن را اجرا کنید، هر تغییری که ایجاد میکنید را بازنویسی میکند. اگر نیاز دارید کد یک وابستگی را تغییر دهید، بسیار بهتر است آن را fork کنید و نسخه fork شده را وارد کنید.
اطلاعات تکمیلی
خطای vendoring ناسازگار
هر بار که یک وابستگی اضافه، حذف یا ارتقا میدهید، مطمئن شوید بلافاصله go mod vendor یا دستور make tidy را اجرا کنید تا محتوای پوشه vendor با فایل go.mod شما همگام شود. اگر این کار را نکنید، با خطای «vendoring ناسازگار» مشابه این مواجه خواهید شد:
$ make run/api
go: inconsistent vendoring in /home/alex/Projects/greenlight:
github.com/foo/bar@v0.1.0: is explicitly
required in go.mod, but not marked as explicit in vendor/modules.txt
To ignore the vendor directory, use -mod=readonly or -mod=mod.
To sync the vendor directory, run:
go mod vendor
الگوی ./…
بیشتر ابزارهای go از الگوی wildcard ./... پشتیبانی میکنند، مانند go fmt ./...، go vet ./... و go test ./.... این الگو دایرکتوری فعلی و تمام زیردایرکتوریها را شامل میشود، به استثنای دایرکتوری vendor.
به طور کلی، این مفید است زیرا به این معنی است که ما کد موجود در دایرکتوری vendor خود را به طور غیرضروری format، vet یا test نمیکنیم — و قاعده make audit ما به دلیل مشکلاتی که ممکن است در آن پکیجهای vendored شده وجود داشته باشد، شکست نخواهد خورد.