زبان برنامهنویسی سلام و Go هر دو زبانهایی کامپایلی با نحو ساده، ساخت سریع، کتابخانهی استاندارد قوی برای شبکه و استقرار با یک فایل اجرایی هستند. تفاوتشان در دو انتخاب بنیادی است. Go گوروتین (Goroutine) دارد، یعنی کارهای سبکی که محیط اجرای خود Go زمانبندیشان میکند، و یک زبالهروب (Garbage Collector) دارد. سلام هیچکدام را ندارد: همزمانی در سلام با نخهای سیستمعامل، یک استخر کار محدود و کانالها انجام میشود و حافظهی هیپ به دست آزاد میشود. همین باعث میشود نوشتن کد بسیار همزمان در Go آسانتر باشد، و سلام سربار حافظهی کمتر و قابل پیشبینیتری داشته باشد، بی هیچ زبالهروبی که کنار برنامه کار کند.
در سنجش مسیریاب HTTP خود پروژه، سرور سلام از سرورهای Go آزمودهشده درخواست بیشتری در ثانیه پاسخ داد، با قیدهایی که این نوشته یکییکی میگوید. با این حال برای بیشتر تیمها، انتخاب میان سلام و Go بیشتر به مدل همزمانی و اکوسیستم بستگی دارد تا به توان عملیاتی.
مقایسهی سلام و Go در یک نگاه
| Go | سلام | |
|---|---|---|
| همزمانی | گوروتینها (زمانبندی M:N در محیط اجرا)، کانالها، select | نخهای سیستمعامل (نخساز)، استخر کار محدود (برو f(x))، کانالها، chan.Select، Futureها |
| ورودی و خروجی مسدودکننده | گوروتین را کنار میگذارد؛ محیط اجرا سوکتها را چندگانهسازی میکند | نخ را مسدود میکند؛ سرور HTTP روی لینوکس از یک حلقهی رویداد با استخر کارگرها استفاده میکند |
| حافظه | با زبالهروب | دستی: .آزادکن() و دیرکن، همراه با بررسی مالکیت در زمان کامپایل |
| خطاها | مقدارهای error، if err != nil | خطادار، تلاش، گرفتن خطا، یا جداشمارهای نتیجه |
| برنامهنویسی عام | از Go 1.18 | بله، با قید میانجی |
| گونههای جمع | ندارد (میانجیها و type switch) | جداشمار با داده و همخوان جامع |
| HTTP | net/http در کتابخانهی استاندارد، چارچوبهایی مانند Gin، Echo و Fiber | net/http و net/router در کتابخانهی استاندارد |
| کامپایل متقابل | GOOS و GOARCH | --target با پیونددهندهی درونی و sysrootهای همراه |
| پختگی | نسخهی ۱٫۰ در ۲۰۱۲، تعهد سازگاری، اکوسیستم عظیم | پیش از ۱٫۰، اکوسیستم کوچک |
همزمانی: گوروتینها در برابر نخها، استخرها و کانالها
این یک استخر کارگر کلاسیک در Go است: سه گوروتین عددها را از یک کانال میخوانند، به توان دو میرسانند و نتیجه را به کانال دیگری میفرستند.
package main
import (
"fmt"
"sync"
)
func main() {
jobs := make(chan int64, 4)
results := make(chan int64, 16)
var wg sync.WaitGroup
for range 3 {
wg.Add(1)
go func() {
defer wg.Done()
for n := range jobs {
results <- n * n
}
}()
}
for i := int64(1); i <= 10; i++ {
jobs <- i
}
close(jobs)
wg.Wait()
close(results)
var sum int64
for v := range results {
sum += v
}
fmt.Println("sum of squares:", sum)
}نسخهی سلام همان شکل را دارد. کارگرها نخهاییاند که با نخساز (در انگلیسی spawn) شروع میشوند، کانالها از بستهی کانال (در انگلیسی chan) میآیند، و دریافت وقتی کانال بسته و خالی باشد مقدار باز را نادرست میکند:
واردسازی کانال
ناپایا کارها := کانال.کانال {} برگردان کانال.کانال<صحیح۶۴>
ناپایا نتیجهها := کانال.کانال {} برگردان کانال.کانال<صحیح۶۴>
تابع کارگر():
ناپایا باز := درست
تا درست:
ن := کارها.دریافت(باز)
اگر وارونه باز:
بشکن
پایان
نتیجهها.فرستادن(ن * ن)
پایان
پایان
تابع ریشه:
کارها.مقداردهی(۴)
نتیجهها.مقداردهی(۱۶)
ناپایا کارگرها := وکتور {} برگردان وکتور<صحیح۶۴>
دیرکن کارگرها.آزادکن()
تکرار ۳:
کارگرها.بیفزا(نخساز(کارگر))
پایان
تکرار ۱ تا ۱۰ در ای:
کارها.فرستادن(ای برگردان صحیح۶۴)
پایان
کارها.بستن()
هر ن در کارگرها:
نخپیوند(ن)
پایان
نتیجهها.بستن()
ناپایا جمع := ۰ برگردان صحیح۶۴
ناپایا باز := درست
تا درست:
م := نتیجهها.دریافت(باز)
اگر وارونه باز:
بشکن
پایان
جمع += م
پایان
سرچاپ "جمع مربعها:"، جمع
کارها.آزادسازی()
نتیجهها.آزادسازی()
پایاناین دو برنامه شبیه هم خوانده میشوند، ولی سازوکار زیرشان یکی نیست. یک گوروتین با پشتهی کوچکی شروع میشود که در صورت نیاز بزرگ میشود، و زمانبند Go میتواند صدها هزار گوروتین را روی چند نخ سیستمعامل اجرا کند و هر گوروتینی را که منتظر کانال یا سوکت است کنار بگذارد. یک نخساز در سلام یک نخ واقعی سیستمعامل با پشتهی ثابت خودش است: برای چند کارگر یا چند صد کارگر مناسب است، نه برای یک نخ به ازای هر اتصال در مقیاس بزرگ.
برای کارهای کوچک و پرشمار، سلام کلیدواژهی go مخصوص خودش را دارد که در سلام فارسی برو نوشته میشود. برو f(x) فراخوانی را به شکل یک کار روی استخری مشترک اجرا میکند که به ازای هر هستهی پردازنده یک کارگر و یک صف محدود دارد؛ وقتی صف پر است، فراخواننده منتظر میماند، پس یک حلقهی سریع نمیتواند کار نامحدود روی هم انباشته کند. اینجا عددهای اول در هشت بازه بهطور موازی شمرده میشوند:
واردسازی اتمی
واردسازی استخر
ناپایا یافتهها := اتمی.صحیح۶۴ جدید(۰ برگردان صحیح۶۴)
تابع اول است(ن: صحیح۶۴): منطقی:
اگر ن < ۲: برگشت نادرست پایان
ناپایا مقسومعلیه := ۲ برگردان صحیح۶۴
تا مقسومعلیه * مقسومعلیه <= ن:
اگر ن % مقسومعلیه == ۰: برگشت نادرست پایان
مقسومعلیه++
پایان
برگشت درست
پایان
تابع شمارش بازه(ابتدا: صحیح۶۴، انتها: صحیح۶۴):
ناپایا ن := ۰ برگردان صحیح۶۴
ناپایا ایکس := ابتدا
تا ایکس < انتها:
اگر اول است(ایکس): ن++ پایان
ایکس++
پایان
_ := اتمی.افزودن(یافتهها، ن)
پایان
تابع ریشه:
تکرار ۸ در ای:
ابتدا := (ای برگردان صحیح۶۴) * ۲۵۰۰۰
برو شمارش بازه(ابتدا، ابتدا + ۲۵۰۰۰)
پایان
استخر.انتظار برو()
سرچاپ "تعداد اعداد اول کمتر از ۲۰۰۰۰۰:"، اتمی.بارگذاری(یافتهها)
اتمی.نابودسازی صحیح۶۴(یافتهها)
پایانمعادل Go از sync.WaitGroup با متد Go آن (که در Go 1.25 اضافه شد) و atomic.Int64 استفاده میکند:
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func isPrime(n int64) bool {
if n < 2 {
return false
}
for d := int64(2); d*d <= n; d++ {
if n%d == 0 {
return false
}
}
return true
}
func main() {
var found atomic.Int64
var wg sync.WaitGroup
for i := range int64(8) {
lo := i * 25000
wg.Go(func() {
var n int64
for x := lo; x < lo+25000; x++ {
if isPrime(x) {
n++
}
}
found.Add(n)
})
}
wg.Wait()
fmt.Println("primes below 200000:", found.Load())
}تفاوتی که باید به خاطر بسپارید: برو در سلام یک استخر نخ است، نه یک زمانبند. کاری که روی یک سوکت یا قفل مسدود شود، یکی از کارگرهای استخر را تا وقتی برگردد در اختیار میگیرد، در حالی که گوروتین مسدود تقریباً هیچ هزینهای برای Go ندارد. سلام بقیهی جعبهابزار را با اینها پر میکند: pool.Async و pool.Await برای Futureها، pool.NewGroup برای گروهی از کارها با سقف همزمانی و گزارش نخستین خطا، chan.Select برای انتظار روی چند کانال با مهلت زمانی، ctx برای لغو و موعد، و sync و atomic برای قفلها و شمارندهها. نوشتهی برنامهنویسی همزمان در سلام و درس نخها این ابزارها را توضیح میدهند. سلام async/await ندارد.
زبالهروب در برابر حافظهی دستی
زبالهروب Go همزمان کار میکند و برای مکثهای کوتاه تنظیم شده است، و بخش بزرگی از دلیل سریع نوشته شدن کد Go همین است: تخصیص میدهید و فراموش میکنید. هزینههایش حاشیهی حافظه (هیپی که جمعآوری میشود میان دو چرخه به فضایی برای رشد نیاز دارد) و زمان پردازندهای است که صرف علامتگذاری میشود، و تنظیم GOGC و GOMEMLIMIT میتواند این دو را با هم معاوضه کند.
سلام زبالهروب ندارد. مقدارها روی پشته زندگی میکنند، ظرفهای هیپ با .آزادکن() آزاد میشوند که معمولاً درست پس از ساختن به شکل دیرکن x.آزادکن() نوشته میشود، و تابعی که رشته یا ظرف تازهای برمیگرداند مالکیت را به فراخواننده میسپارد. بررسیگر مالکیت کامپایلر استفاده پس از آزادسازی، آزادسازی دوباره و اشتباههای مشابه را گزارش میکند و salam memcheck برنامه را زیر AddressSanitizer اجرا میکند. مصرف حافظه همان چیزی است که برنامه تخصیص میدهد، بدون سربار زبالهروب، ولی فراموش کردن یک آزادسازی نشتی است که فقط آزمونها یا ابزارها پیدایش میکنند، و در سروری که هفتهها اجرا میشود این مهم است. نوشتهی مدیریت حافظه در سلام قاعدهها را توضیح میدهد.
نوشتن وبسرور با Go و با زبان سلام
هر دو زبان یک سرور HTTP در کتابخانهی استاندارد دارند. در Go، با الگوهای مسیری که در Go 1.22 اضافه شد:
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /hello/{name}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!\n", r.PathValue("name"))
})
log.Fatal(http.ListenAndServe(":8080", mux))
}در سلام، با net/http (به فارسی شبکه.اچ تی تی پی) و net/router (به فارسی شبکه.مسیریاب):
واردسازی شبکه.اچ تی تی پی
واردسازی شبکه.مسیریاب
تابع سلام کن(زمینه: صحیح۶۴):
اچ تی تی پی.متن(زمینه، "سلام " + اچ تی تی پی.پارامتر(زمینه، "name") + "!\n")
پایان
تابع ریشه:
ناپایا م := مسیریاب.مسیریاب جدید()
مسیریاب.دریافت(م، "/hello/:name"، سلام کن)
ناپایا س := اچ تی تی پی.سرور جدید(۸۰۸۰)
اچ تی تی پی.استفاده(س، م)
اچ تی تی پی.اجرا(س)
پایانهر دو به curl localhost:8080/hello/Sara پاسخ میدهند (Go با Hello, Sara! و این نسخهی فارسی با سلام Sara!) و برای مسیرهای ناشناخته ۴۰۴ برمیگردانند. گردانندهی سلام یک شناسهی زمینه (Context) میگیرد و پارامترها را با تابع میخواند؛ گردانندهی Go یک نویسنده و یک شیء درخواست میگیرد. کتابخانهی استاندارد سلام میانافزار، نشست، اعتبارسنجی، تولید OpenAPI، WebSocket، HTTP/2 و gRPC را هم دارد، و salam serve --config یک سرور چندسایتی با میزبانهای مجازی و پشتیبانهای FastCGI اجرا میکند. یک قاعده با Go فرق دارد و بهآسانی از چشم میافتد: رشتههایی که از درخواست خوانده میشوند وقتی گرداننده برمیگردد آزاد میشوند، پس هر چیزی را که نگه میدارید باید با str.Clone کپی کنید. نوشتهی ساخت وبسرورهای سریع با سلام عمیقتر به این موضوع میپردازد.
سنجش مسیریاب HTTP چه چیزی را نشان میدهد؟
پروژه سنجشی از نه سرور منتشر کرده که به همان یازده مسیر پاسخ میدهند (متن ساده، JSON، یک پاسخ از حافظهی نهان، یک پرونده، یک پارامتر مسیر، تجزیهی پرسمان، یک محاسبه، سرآیندها، یک پروندهی ایستا، صفحهی ریشه و بازتاب یک درخواست POST). در اجرای ۶ آبان ۱۴۰۵ (۲۸ اکتبر ۲۰۲۶)، میانگین هندسی روی هر یازده مسیر این بود:
| سرور | درخواست در ثانیه |
|---|---|
net/http سلام | ۹۴٬۸۵۰ |
| Go، Fiber | ۶۲٬۴۹۳ |
| Go، Echo | ۳۵٬۲۳۸ |
| Go، Gin | ۳۴٬۹۹۹ |
Go، net/http | ۳۴٬۹۸۷ |
| Node، Express | ۲۲٬۵۳۶ |
| سرور توکار PHP | ۱۱٬۹۹۲ |
سلام در هر ۱۱ مسیر از Fiber درخواست بیشتری در ثانیه پاسخ داد و ۲٫۷ برابر net/http خود Go بود. بیشترین فاصله در تجزیهی پرسمان همراه با ساختن فهرست بود (/search، ۹۹٬۱۴۳ در برابر ۴۸٬۲۴۷ برای Fiber) و کمترین فاصله در /، جایی که سلام ۴٪ جلوتر بود.
حالا قیدها، که به اندازهی خود جدول اهمیت دارند:
- اجرا روی یک محیط اشتراکی با ۶ پردازندهی مجازی AMD EPYC و میانگین بار حدود ۱۱ از کارهای نامربوط انجام شد؛ سرورها روی ۴ هسته و ابزار تولید بار روی ۲ هسته قفل شده بودند، با ۶۴ اتصال keep-alive و ۱۰ ثانیه برای هر مسیر پس از ۳ ثانیه گرمکردن، و مسیرها بهنوبت روی همهی سرورها سنجیده شدند.
- بار پسزمینه به سرورهایی که نخهای کمی نگه میدارند، از جمله سلام، کمک میکند. برای همین صفحهی سرعت فاصله با Fiber را ۱۶٪ روی دستگاه آرام و ۵۲٪ در اجرای کامل زیر بار گزارش میکند.
- Fiber با پیکربندی پیشفرضش (بدون prefork) روی Go 1.26.0 اجرا شد. سرورهای Go را میشود تنظیم کرد، و سرور سلام را هم.
- پروندهی
BENCHMARK.mdابزار سنجش درtests/en/apps/httpbenchاکنون اجرای بعدیای روی یک اجراکنندهی ۴ پردازندهای GitHub را نگه میدارد که در آن فاصله با Fiber بسیار کمتر است و Fiber در برخی مسیرها جلوتر است، بهروشنترین شکل در پروندههای ایستا.
خوانش منصفانه این است که سرور سلام روی مسیرهای کوچک و کممصرف از نظر پردازنده همردهی سریعترین چارچوبهای Go است، و در این اجراها با فاصلهی راحتی جلوتر از سرور کتابخانهی استاندارد Go و Node. این گواهی نیست که سلام برنامهی شما را سریعتر سرو میکند: سرویسهای واقعی وقتشان را در پایگاه داده، سریالسازی و منطق کسبوکار میگذرانند، و این سنجش اتصالهای طولانیمدت در مقیاس بزرگ را، که گوروتینها در آن میدرخشند، نمیآزماید. پیش از برنامهریزی ظرفیت، آن را روی سختافزار خودتان دوباره اجرا کنید؛ صفحهی سرعت به ابزار سنجش پیوند داده است.
سرعت در کارهای پردازندهمحور
در سنجش جداگانهی ۳۹ برنامهی کوچک با الگوریتم یکسان در همهی زبانها، میانگین سلام ۱۰٫۶ میلیثانیه و میانگین Go ۱۴٫۲ میلیثانیه بود، یعنی Go روی این مجموعه حدود ۱٫۳ برابر کندتر است. Go در هیچکدام از این برنامهها سریعترین زبان کامپایلی نبود. هر اجرا یک فرایند کامل و کوتاهعمر است، پس راهاندازی محیط اجرای Go بخشی از هر عدد است، و سنجش این را از کیفیت کد تولیدشده جدا نمیکند. خوانش دقیق سنجش سرعت توضیح میدهد چرا چنین میانگینهایی یک ردهاند، نه رتبهبندی.
ابزارها، استقرار و اکوسیستم
هر دو زبان فایلهای اجرایی تکی میسازند که استقرارشان آسان است، و هر دو از یک دستگاه برای سکوهای دیگر کامپایل میکنند: Go با GOOS و GOARCH، و سلام با --target و پیونددهندهی درونیاش (کامپایل متقابل در سلام را ببینید). هر دو قالببند، اجراکنندهی آزمون و ابزار مستندسازی را درون فرمان اصلی دارند.
اکوسیستمها قابل مقایسه نیستند. Go یک دهه و نیم سابقهی استفاده در محیط عملیاتی، تعهد سازگاری برای نسخههای 1.x، کتابخانههای پختهی پایش و ابر، و بازار بزرگ نیروی کار دارد. سلام یک کتابخانهی استاندارد گسترده، یک مدیر بسته که بستهها را از GitHub نصب میکند، و جامعهای کوچک دارد. اگر سرویس شما امروز به یک SDK ابری، سامانهی ردیابی یا درایور پایگاه دادهی مشخص نیاز دارد، پیش از تصمیم گرفتن بررسی کنید که سلام آن را دارد.
کی Go و کی زبان سلام را انتخاب کنیم؟
Go را انتخاب کنید وقتی:
- به همزمانی بسیار بالا با کد ساده نیاز دارید: دهها هزار اتصال، هر کدام در گوروتین خودش؛
- تیم زبالهروب را به مدیریت دستی حافظه ترجیح میدهد؛
- به اکوسیستم Go وابستهاید: SDKهای ابری، ابزارهای Kubernetes، کتابخانههای پایش؛
- پایداری بلندمدت و استخدام از کارایی خام مهمترند.
سلام را انتخاب کنید وقتی:
- سرعت زبانهای کامپایلی را بدون زبالهروب و با مصرف حافظهی قابل پیشبینی میخواهید؛
- مسیرهای سرورتان از نظر پردازنده سبکاند و توان عملیاتیای را که سنجش نشان میدهد میخواهید، پس از آنکه آن را روی سختافزار خودتان تأیید کردید؛
- گونههای جمع، تطبیق الگوی جامع و کتابخانهی استاندارد پربار را در زبانی میخواهید که همچنان ساده است؛
- میخواهید به فارسی برنامه بنویسید یا آموزش دهید.
پرسشهای پرتکرار
آیا زبان سلام گوروتین دارد؟
نه. برو f(x) (در انگلیسی go f(x)) فراخوانی را به شکل یک کار روی یک استخر نخ محدود اجرا میکند و نخساز یک نخ سیستمعامل شروع میکند. زمانبند M:N وجود ندارد، پس کاری که مسدود شود نخ کارگرش را تا بازگشت در اختیار نگه میدارد.
آیا سلام برای وبسرور از Go سریعتر است؟
در سنجش مسیریاب پروژه، net/http سلام از Fiber، Gin، Echo و net/http خود Go درخواست بیشتری در ثانیه پاسخ داد، ولی فاصله بهشدت به دستگاه و بار آن وابسته بود و اجرای بعدی فاصلهی بسیار کمتری نشان داد. پیش از تصمیمگیری، بار کاری خودتان را بسنجید.
آیا زبان سلام زبالهروب (Garbage Collector) دارد؟
نه. حافظه با .آزادکن() و دیرکن به دست مدیریت میشود و کامپایلر اشتباههای رایج را در زمان کامپایل بررسی میکند. این از سربار زبالهروب جلوگیری میکند، ولی نشتی حافظه را ممکن میسازد.
آیا میشود در سلام مثل Go از کانال استفاده کرد؟
بله. بستهی کانال (در انگلیسی chan) کانالهای میانگیردار و بیمیانگیر را با فرستادن، دریافت، بستن، مهلتهای زمانی و chan.Select روی چند کانال فراهم میکند. کانالها مقدارها را میان نخها یا کارهای استخر جابهجا میکنند، نه میان گوروتینها.