ساختار و سازماندهی پروژه (Project Structure and Organization)
قبل از اینکه کد بیشتری به فایل main.go اضافه کنیم، زمان خوبی است که به نحوه سازماندهی و ساختاردهی این پروژه فکر کنیم.
مهم است که از ابتدا توضیح دهیم که هیچ روش درست یا حتی توصیهشدهای برای ساختاردهی برنامههای وب در Go وجود ندارد. و این هم خوب است و هم بد. این به این معناست که شما آزادی و انعطافپذیری در نحوه سازماندهی کد خود دارید، اما همچنین به راحتی میتوانید در یک مسیر نامشخص گیر کنید وقتی که سعی میکنید بهترین ساختار را انتخاب کنید.
با کسب تجربه در Go، شما احساس خواهید کرد که کدام الگوها در موقعیتهای مختلف برای شما خوب کار میکنند. اما به عنوان نقطه شروع، بهترین توصیهای که میتوانم به شما بدهم این است که چیزها را بیش از حد پیچیده نکنید. سعی کنید سخت تلاش کنید تا ساختار و پیچیدگی را فقط زمانی اضافه کنید که به وضوح نیاز باشد.
برای این پروژه، ما یک ساختار کلی را پیادهسازی خواهیم کرد که از یک رویکرد محبوب و آزمایششده (Popular and Proven Approach) پیروی میکند. این یک نقطه شروع محکم است و شما باید بتوانید ساختار کلی را در طیف وسیعی از پروژهها مجدداً استفاده کنید.
اگر شما هم دنبال میکنید، مطمئن شوید که در ریشه مخزن پروژه خود هستید و دستورات زیر را اجرا کنید:
$ cd $HOME/code/snippetbox $ rm main.go $ mkdir -p cmd/web internal ui/html ui/static $ touch cmd/web/main.go $ touch cmd/web/handlers.go
ساختار مخزن پروژه شما اکنون باید به این صورت باشد:
بیایید لحظهای وقت بگذاریم تا در مورد اینکه هر یک از این دایرکتوریها برای چه استفاده میشوند صحبت کنیم.
دایرکتوری
cmdشامل کد خاص برنامه (Application-specific Code) برای برنامههای اجرایی در پروژه خواهد بود. فعلاً پروژه ما فقط یک برنامه اجرایی خواهد داشت — برنامه وب — که در دایرکتوریcmd/webقرار خواهد گرفت.دایرکتوری
internalشامل کد غیر خاص برنامه (Non-application-specific Code) کمکی استفادهشده در پروژه خواهد بود. ما از آن برای نگهداری کدهای قابل استفاده مجدد مانند کمککنندههای اعتبارسنجی و مدلهای پایگاه داده SQL برای پروژه استفاده خواهیم کرد.دایرکتوری
uiشامل داراییهای رابط کاربری (UI Assets) استفادهشده توسط برنامه وب خواهد بود. به طور خاص، دایرکتوریui/htmlشامل قالبهای HTML خواهد بود و دایرکتوریui/staticشامل فایلهای استاتیک (Static Files) (مانند CSS و تصاویر) خواهد بود.
پس چرا از این ساختار استفاده میکنیم؟
دو مزیت بزرگ وجود دارد:
این یک جداسازی تمیز بین داراییهای Go و غیر Go ایجاد میکند. تمام کد Go که مینویسیم به طور انحصاری در دایرکتوریهای
cmdوinternalقرار خواهد گرفت و ریشه پروژه را آزاد میگذارد تا داراییهای غیر Go مانند فایلهای رابط کاربری، فایلهای ساخت و تعریف ماژولها (از جمله فایلgo.modما) را نگه دارد.این به خوبی مقیاسپذیر است اگر بخواهید یک برنامه اجرایی دیگر به پروژه خود اضافه کنید. به عنوان مثال، ممکن است بخواهید یک CLI (رابط خط فرمان) برای خودکارسازی برخی از وظایف مدیریتی در آینده اضافه کنید. با این ساختار، میتوانید این برنامه CLI را در زیر
cmd/cliایجاد کنید و قادر خواهید بود تمام کدی که در زیر دایرکتوریinternalنوشتهاید را وارد و مجدداً استفاده کنید.
بازسازی کد موجود شما (Refactoring Your Existing Code)
بیایید به سرعت کدی که قبلاً نوشتهایم را به این ساختار جدید منتقل کنیم.
package main import ( "log" "net/http" ) func main() { mux := http.NewServeMux() mux.HandleFunc("GET /{$}", home) mux.HandleFunc("GET /snippet/view/{id}", snippetView) mux.HandleFunc("GET /snippet/create", snippetCreate) mux.HandleFunc("POST /snippet/create", snippetCreatePost) log.Print("starting server on :4000") err := http.ListenAndServe(":4000", mux) log.Fatal(err) }
package main import ( "fmt" "net/http" "strconv" ) func home(w http.ResponseWriter, r *http.Request) { w.Header().Add("Server", "Go") w.Write([]byte("Hello from Snippetbox")) } func snippetView(w http.ResponseWriter, r *http.Request) { id, err := strconv.Atoi(r.PathValue("id")) if err != nil || id < 1 { http.NotFound(w, r) return } fmt.Fprintf(w, "Display a specific snippet with ID %d...", id) } func snippetCreate(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Display a form for creating a new snippet...")) } func snippetCreatePost(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) w.Write([]byte("Save a new snippet...")) }
بنابراین اکنون برنامه وب ما شامل چندین فایل .go در زیر دایرکتوری cmd/web است. برای اجرای اینها، میتوانیم از دستور go run به این صورت استفاده کنیم:
$ cd $HOME/code/snippetbox $ go run ./cmd/web 2024/03/18 11:29:23 starting server on :4000
اطلاعات اضافی
دایرکتوری internal (The Internal Directory)
مهم است که اشاره کنیم که نام دایرکتوری internal در Go معنای خاص و رفتار خاصی دارد: هر بستهای که در زیر این دایرکتوری قرار دارد فقط میتواند توسط کد داخل والد دایرکتوری internal وارد شود. در مورد ما، این به این معناست که هر بستهای که در internal قرار دارد فقط میتواند توسط کد داخل دایرکتوری پروژه snippetbox وارد شود.
یا، از طرف دیگر، این به این معناست که هر بستهای که در internal قرار دارد نمیتواند توسط کد خارج از پروژه ما وارد شود.
این مفید است زیرا از وارد کردن و وابستگی دیگر کدها به بستههای (احتمالاً بدون نسخه و پشتیبانی نشده) در دایرکتوری internal جلوگیری میکند — حتی اگر کد پروژه به صورت عمومی در جایی مانند GitHub در دسترس باشد.
واژهنامه اصطلاحات فنی
| اصطلاح فارسی | معادل انگلیسی | توضیح |
|---|---|---|
| ساختار و سازماندهی پروژه | Project Structure and Organization | نحوه چیدمان و سازماندهی فایلها و دایرکتوریهای یک پروژه نرمافزاری |
| کد خاص برنامه | Application-specific Code | کدی که مختص به یک برنامه خاص است و قابل استفاده مجدد در برنامههای دیگر نیست |
| کد غیر خاص برنامه | Non-application-specific Code | کدی که میتواند در چندین برنامه مورد استفاده مجدد قرار گیرد |
| داراییهای رابط کاربری | UI Assets | فایلهایی که برای ساخت رابط کاربری استفاده میشوند، مانند قالبهای HTML و فایلهای CSS |
| فایلهای استاتیک | Static Files | فایلهایی که بدون تغییر به کاربر ارسال میشوند، مانند تصاویر و فایلهای CSS |
| رویکرد محبوب و آزمایششده | Popular and Proven Approach | روشی که توسط بسیاری از توسعهدهندگان استفاده شده و کارایی آن ثابت شده است |
| بازسازی کد | Code Refactoring | تغییر ساختار کد بدون تغییر در عملکرد آن |
| برنامه اجرایی | Executable Program | برنامهای که میتواند به طور مستقیم توسط سیستم عامل اجرا شود |
| کد قابل استفاده مجدد | Reusable Code | کدی که میتواند در بخشهای مختلف یک برنامه یا در برنامههای مختلف استفاده شود |
| مقیاسپذیری | Scalability | توانایی یک ساختار برای رشد و گسترش بدون نیاز به تغییرات اساسی |