Go / Эцсийн төсөл

Эцсийн төсөл

Баяр хүргэе! Та Go хэлний урт аялалыг туулж ирлээ — хувьсагч, функц, struct, interface, goroutine, channel, HTTP server, database, тест, benchmark, generic, context, deploy... Бүгдийг судаллаа. Одоо цаг нь боллоо: эдгээр бүх мэдлэгийг нэг бүрэн ажилладаг программд нэгтгэнэ. Энэ хичээлд номын сангийн REST API сервер бичнэ — номуудыг нэмэх, жагсаах, хайх, устгах боломжтой.

Төслийн бүтэц

Бодит Go програм нэг файлд бичдэггүй. Логикийг хэд хэдэн файлд тараах нь уншихад, өргөтгөхөд хялбар болгодог:

код
library-api/
├── main.go           ← программ эхлэх цэг
├── go.mod
├── handler/
│   └── book.go       ← HTTP handler-ууд
├── store/
│   └── book.go       ← database логик
└── model/
    └── book.go       ← өгөгдлийн бүтэц
bash
# Төсөл үүсгэх
mkdir library-api && cd library-api
go mod init library-api
go get modernc.org/sqlite

Өгөгдлийн бүтэц — model

model/book.go файлд номын бүтцийг тодорхойлно:

go
// model/book.go
package model

import "time"

type Ном struct {
    ID        int       `json:"id"`
    Гарчиг    string    `json:"гарчиг"`
    Зохиолч   string    `json:"зохиолч"`
    Он        int       `json:"он"`
    НэмэгдсэнОгноо time.Time `json:"нэмэгдсэн_огноо"`
}

type НомҮүсгэхОролт struct {
    Гарчиг  string `json:"гарчиг"`
    Зохиолч string `json:"зохиолч"`
    Он      int    `json:"он"`
}

func (о *НомҮүсгэхОролт) Шалгах() error {
    if о.Гарчиг == "" {
        return fmt.Errorf("гарчиг заавал шаардлагатай")
    }
    if о.Зохиолч == "" {
        return fmt.Errorf("зохиолч заавал шаардлагатай")
    }
    if о.Он < 1000 || о.Он > 2100 {
        return fmt.Errorf("он буруу байна: %d", о.Он)
    }
    return nil
}

Database давхарга — store

store/book.go файлд бүх database үйлдлүүдийг нэгтгэнэ:

go
// store/book.go
package store

import (
    "context"
    "database/sql"
    "fmt"
    "library-api/model"

    _ "modernc.org/sqlite"
)

type НомийнСан struct {
    db *sql.DB
}

func Шинэ(db *sql.DB) *НомийнСан {
    return &НомийнСан{db: db}
}

func (с *НомийнСан) ХүснэгтҮүсгэх(ctx context.Context) error {
    _, err := с.db.ExecContext(ctx, `
        CREATE TABLE IF NOT EXISTS номнууд (
            id         INTEGER PRIMARY KEY AUTOINCREMENT,
            гарчиг     TEXT    NOT NULL,
            зохиолч    TEXT    NOT NULL,
            он         INTEGER NOT NULL,
            нэмэгдсэн  DATETIME DEFAULT CURRENT_TIMESTAMP
        )
    `)
    return err
}

func (с *НомийнСан) БүгдийгАвах(ctx context.Context) ([]model.Ном, error) {
    rows, err := с.db.QueryContext(ctx,
        "SELECT id, гарчиг, зохиолч, он, нэмэгдсэн FROM номнууд ORDER BY id DESC",
    )
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var номнууд []model.Ном
    for rows.Next() {
        var н model.Ном
        err := rows.Scan(&н.ID, &н.Гарчиг, &н.Зохиолч, &н.Он, &н.НэмэгдсэнОгноо)
        if err != nil {
            return nil, err
        }
        номнууд = append(номнууд, н)
    }
    return номнууд, rows.Err()
}

func (с *НомийнСан) НэмэхНом(ctx context.Context, оролт model.НомҮүсгэхОролт) (model.Ном, error) {
    result, err := с.db.ExecContext(ctx,
        "INSERT INTO номнууд (гарчиг, зохиолч, он) VALUES (?, ?, ?)",
        оролт.Гарчиг, оролт.Зохиолч, оролт.Он,
    )
    if err != nil {
        return model.Ном{}, err
    }

    id, _ := result.LastInsertId()
    return с.IDАарАвах(ctx, int(id))
}

func (с *НомийнСан) IDАарАвах(ctx context.Context, id int) (model.Ном, error) {
    var н model.Ном
    err := с.db.QueryRowContext(ctx,
        "SELECT id, гарчиг, зохиолч, он, нэмэгдсэн FROM номнууд WHERE id = ?", id,
    ).Scan(&н.ID, &н.Гарчиг, &н.Зохиолч, &н.Он, &н.НэмэгдсэнОгноо)

    if err == sql.ErrNoRows {
        return н, fmt.Errorf("ном олдсонгүй: %d", id)
    }
    return н, err
}

func (с *НомийнСан) Устгах(ctx context.Context, id int) error {
    result, err := с.db.ExecContext(ctx, "DELETE FROM номнууд WHERE id = ?", id)
    if err != nil {
        return err
    }
    тоо, _ := result.RowsAffected()
    if тоо == 0 {
        return fmt.Errorf("ном олдсонгүй: %d", id)
    }
    return nil
}

HTTP handler давхарга

handler/book.go файлд HTTP хүсэлтүүдийг боловсруулна:

go
// handler/book.go
package handler

import (
    "encoding/json"
    "library-api/model"
    "library-api/store"
    "net/http"
    "strconv"
)

type НомийнHandler struct {
    сан *store.НомийнСан
}

func Шинэ(сан *store.НомийнСан) *НомийнHandler {
    return &НомийнHandler{сан: сан}
}

func (h *НомийнHandler) ЖагсаалтАвах(w http.ResponseWriter, r *http.Request) {
    номнууд, err := h.сан.БүгдийгАвах(r.Context())
    if err != nil {
        http.Error(w, `{"алдаа":"серверийн алдаа"}`, http.StatusInternalServerError)
        return
    }
    if номнууд == nil {
        номнууд = []model.Ном{} // null бус хоосон array буцаах
    }
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(номнууд)
}

func (h *НомийнHandler) НэмэхНом(w http.ResponseWriter, r *http.Request) {
    var оролт model.НомҮүсгэхОролт
    if err := json.NewDecoder(r.Body).Decode(&оролт); err != nil {
        http.Error(w, `{"алдаа":"буруу JSON"}`, http.StatusBadRequest)
        return
    }
    if err := оролт.Шалгах(); err != nil {
        http.Error(w, `{"алдаа":"`+err.Error()+`"}`, http.StatusBadRequest)
        return
    }
    ном, err := h.сан.НэмэхНом(r.Context(), оролт)
    if err != nil {
        http.Error(w, `{"алдаа":"серверийн алдаа"}`, http.StatusInternalServerError)
        return
    }
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(ном)
}

func (h *НомийнHandler) Устгах(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        http.Error(w, `{"алдаа":"буруу ID"}`, http.StatusBadRequest)
        return
    }
    if err := h.сан.Устгах(r.Context(), id); err != nil {
        http.Error(w, `{"алдаа":"ном олдсонгүй"}`, http.StatusNotFound)
        return
    }
    w.WriteHeader(http.StatusNoContent)
}

func (h *НомийнHandler) RouteҮүсгэх(mux *http.ServeMux) {
    mux.HandleFunc("GET /api/books", h.ЖагсаалтАвах)
    mux.HandleFunc("POST /api/books", h.НэмэхНом)
    mux.HandleFunc("DELETE /api/books/{id}", h.Устгах)
}

Бүгдийг нэгтгэх — main.go

go
// main.go
package main

import (
    "context"
    "database/sql"
    "fmt"
    "library-api/handler"
    "library-api/store"
    "log"
    "net/http"
    "os"
    "time"

    _ "modernc.org/sqlite"
)

func логгMiddleware(дараачийн http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        эхлэл := time.Now()
        дараачийн.ServeHTTP(w, r)
        fmt.Printf("[%s] %s %s %v\n",
            time.Now().Format("15:04:05"),
            r.Method, r.URL.Path,
            time.Since(эхлэл),
        )
    })
}

func main() {
    // Database нэхэх
    db, err := sql.Open("sqlite", "library.db")
    if err != nil {
        log.Fatal("Database нээхэд алдаа:", err)
    }
    defer db.Close()

    // Хүснэгт үүсгэх
    сан := store.Шинэ(db)
    ctx, цуцлах := context.WithTimeout(context.Background(), 5*time.Second)
    defer цуцлах()
    if err := сан.ХүснэгтҮүсгэх(ctx); err != nil {
        log.Fatal("Хүснэгт үүсгэхэд алдаа:", err)
    }

    // Router болон handler тохируулах
    mux := http.NewServeMux()
    номHandler := handler.Шинэ(сан)
    номHandler.RouteҮүсгэх(mux)

    // Порт
    порт := os.Getenv("PORT")
    if порт == "" {
        порт = "8080"
    }

    fmt.Printf("📚 Номын сангийн API :%s дээр ажиллаж байна\n", порт)
    log.Fatal(http.ListenAndServe(":"+порт, логгMiddleware(mux)))
}
bash
# Ажиллуулах
go run .

# Ном нэмэх
curl -X POST http://localhost:8080/api/books \
  -H "Content-Type: application/json" \
  -d '{"гарчиг":"Монгол нууц товчоо","зохиолч":"Тэргүүн","он":1240}'

# Жагсаалт авах
curl http://localhost:8080/api/books

# Ном устгах
curl -X DELETE http://localhost:8080/api/books/1

Дараагийн хичээлд:

Та Go курсийг амжилттай дуусгалаа! 🎉

Энэ курсэд та Go хэлний суурь бүтцээс эхлэн goroutine, channel, interface, HTTP server, database, тест, benchmark, generic, context хүртэл бүгдийг судаллаа. Одоо та дэлхийн зах зээлд өрсөлдөх чадвартай Go хөгжүүлэгч болсон.

Цаашдын алхам:

  • Өөрийн санаачилсан жинхэнэ төсөл бичиж эхэл — энэ л хамгийн сайн багш
  • gin эсвэл chi router судла
  • PostgreSQL-тэй холбож тест хий
  • GitHub дээр кодоо нийтэл — ажил олгогчид ажлын туршлагатай адил үнэлдэг

Амжилт хүсье!