Tarkibga o'tish

Loyiha: CRUD

Oldingi darslarda net/http paketining alohida qismlarini o‘rgandik: handler, router, so‘rov va javob, yo‘l hamda query parametrlar. Endi ularni bitta loyihada birlashtiramiz va kitoblar uchun to‘liq ishlaydigan API yozamiz.

CRUD — ma’lumot ustida bajariladigan to‘rtta asosiy amalning qisqartmasi:

Amal Ma’nosi HTTP Endpoint
Create Yaratish POST /books
Read O‘qish GET /books, /books/{id}
Update Yangilash PUT /books/{id}
Delete O‘chirish DELETE /books/{id}

Deyarli har qanday ilovaning asosida CRUD turadi. Blogda maqola yoziladi, o‘qiladi, tahrirlanadi va o‘chiriladi. Do‘konda mahsulot, bankda hisob, kutubxonada kitob — barchasi shu to‘rt amal atrofida quriladi.

Loyiha faqat standart kutubxonadan foydalanadi. Ma’lumotlar hozircha xotirada (map ichida) saqlanadi. Keyingi bo‘limlarda ularni PostgreSQL’ga ko‘chiramiz.

Loyihani tayyorlash

mkdir books-api
cd books-api
go mod init books-api

Barcha kod main.go faylida bo‘ladi. Kodni qismlarga bo‘lib tushuntiramiz, to‘liq fayl esa dars oxirida keltirilgan.

1. Model

Avval API qanday ma’lumot bilan ishlashini belgilaymiz:

type Book struct {
    ID        int       `json:"id"`
    Title     string    `json:"title"`
    Author    string    `json:"author"`
    Year      int       `json:"year"`
    CreatedAt time.Time `json:"created_at"`
}

type bookInput struct {
    Title  string `json:"title"`
    Author string `json:"author"`
    Year   int    `json:"year"`
}

Book — saqlanadigan va klientga qaytariladigan to‘liq kitob. bookInput — klient yaratish yoki yangilash paytida yuboradigan ma’lumot.

Nima uchun ikkita alohida tur? ID va CreatedAt qiymatlarini server o‘zi belgilaydi. Agar so‘rov tanasi to‘g‘ridan-to‘g‘ri Bookga o‘qilsa, klient "id": 999 yuborib, ID’ni o‘zi tanlab olishi mumkin bo‘lardi. Alohida kirish turi bunday holatning oldini oladi.

2. Ma’lumotlar ombori (store)

Kitoblar map ichida saqlanadi. Server har bir so‘rovni alohida goroutine’da qayta ishlaydi. Shu sababli map’ga murojaat sync.RWMutex bilan himoyalanadi:

type store struct {
    mu     sync.RWMutex
    books  map[int]Book
    nextID int
}

func newStore() *store {
    return &store{books: make(map[int]Book), nextID: 1}
}

RWMutex ikki xil qulf beradi. RLock — o‘qish uchun: bir vaqtda ko‘p goroutine o‘qishi mumkin. Lock — yozish uchun: yozuvchi ishlayotganda boshqa hech kim o‘qiy olmaydi va yoza olmaydi. API’da odatda o‘qish yozishdan ancha ko‘p bo‘ladi, shuning uchun RWMutex mos keladi.

Omborning metodlari:

var errNotFound = errors.New("kitob topilmadi")

func (s *store) list() []Book {
    s.mu.RLock()
    defer s.mu.RUnlock()

    books := make([]Book, 0, len(s.books))
    for _, b := range s.books {
        books = append(books, b)
    }
    slices.SortFunc(books, func(a, b Book) int { return a.ID - b.ID })
    return books
}

func (s *store) get(id int) (Book, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()

    b, ok := s.books[id]
    if !ok {
        return Book{}, errNotFound
    }
    return b, nil
}

func (s *store) create(in bookInput) Book {
    s.mu.Lock()
    defer s.mu.Unlock()

    b := Book{
        ID:        s.nextID,
        Title:     in.Title,
        Author:    in.Author,
        Year:      in.Year,
        CreatedAt: time.Now().UTC(),
    }
    s.books[b.ID] = b
    s.nextID++
    return b
}

func (s *store) update(id int, in bookInput) (Book, error) {
    s.mu.Lock()
    defer s.mu.Unlock()

    b, ok := s.books[id]
    if !ok {
        return Book{}, errNotFound
    }
    b.Title, b.Author, b.Year = in.Title, in.Author, in.Year
    s.books[id] = b
    return b, nil
}

func (s *store) delete(id int) error {
    s.mu.Lock()
    defer s.mu.Unlock()

    if _, ok := s.books[id]; !ok {
        return errNotFound
    }
    delete(s.books, id)
    return nil
}

E’tibor bering:

  • list natijani ID bo‘yicha saralaydi. Map elementlari tasodifiy tartibda aylanadi, saralanmasa har safar boshqa tartib chiqadi.
  • list bo‘sh holatda nil emas, bo‘sh slice qaytaradi. JSON’da nil slice null, bo‘sh slice esa [] bo‘ladi. Klient uchun [] qulayroq.
  • Topilmagan holat uchun errNotFound xatosi bor. Handler shu xatoga qarab 404 qaytaradi.

3. Yordamchi funksiyalar

Har bir handler JSON yozadi, xato qaytaradi va ID o‘qiydi. Bir xil kodni takrorlamaslik uchun uchta yordamchi funksiya yozamiz:

func writeJSON(w http.ResponseWriter, status int, data any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    if err := json.NewEncoder(w).Encode(data); err != nil {
        log.Printf("JSON yozishda xato: %v", err)
    }
}

func writeError(w http.ResponseWriter, status int, msg string) {
    writeJSON(w, status, map[string]string{"error": msg})
}

func parseID(r *http.Request) (int, error) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil || id <= 0 {
        return 0, errors.New("id musbat butun son bo‘lishi kerak")
    }
    return id, nil
}

Kirish ma’lumotini o‘qish va tekshirish uchun yana bitta funksiya:

func decodeBook(w http.ResponseWriter, r *http.Request) (bookInput, error) {
    r.Body = http.MaxBytesReader(w, r.Body, 1<<20)

    var in bookInput
    dec := json.NewDecoder(r.Body)
    dec.DisallowUnknownFields()
    if err := dec.Decode(&in); err != nil {
        return in, errors.New("JSON noto‘g‘ri: " + err.Error())
    }

    in.Title = strings.TrimSpace(in.Title)
    in.Author = strings.TrimSpace(in.Author)

    switch {
    case in.Title == "":
        return in, errors.New("title majburiy")
    case in.Author == "":
        return in, errors.New("author majburiy")
    case in.Year < 0 || in.Year > time.Now().Year():
        return in, errors.New("year noto‘g‘ri")
    }
    return in, nil
}
  • MaxBytesReader tanani 1 MB bilan cheklaydi.
  • DisallowUnknownFields notanish maydonlarni rad etadi. Masalan, "titel" deb xato yozilgan maydon jimgina e’tiborsiz qolmaydi.
  • TrimSpace bo‘shliqlarni olib tashlaydi. Aks holda " " ham to‘g‘ri sarlavha deb qabul qilinardi.

Bu tekshiruvlar hozircha qo‘lda yozildi. JSON bo‘limida ularni ancha qulayroq usullar bilan almashtiramiz.

4. Handlerlar

Handlerlar storega murojaat qilishi kerak. Buning uchun ularni server struct metodlari sifatida yozamiz. Global o‘zgaruvchi ishlatilmaydi:

type server struct {
    store *store
}

Ro‘yxat: GET /books

func (s *server) listBooks(w http.ResponseWriter, r *http.Request) {
    writeJSON(w, http.StatusOK, s.store.list())
}

Bitta kitob: GET /books/{id}

func (s *server) getBook(w http.ResponseWriter, r *http.Request) {
    id, err := parseID(r)
    if err != nil {
        writeError(w, http.StatusBadRequest, err.Error())
        return
    }

    b, err := s.store.get(id)
    if errors.Is(err, errNotFound) {
        writeError(w, http.StatusNotFound, err.Error())
        return
    }

    writeJSON(w, http.StatusOK, b)
}

Yaratish: POST /books

func (s *server) createBook(w http.ResponseWriter, r *http.Request) {
    in, err := decodeBook(w, r)
    if err != nil {
        writeError(w, http.StatusBadRequest, err.Error())
        return
    }

    b := s.store.create(in)
    w.Header().Set("Location", fmt.Sprintf("/books/%d", b.ID))
    writeJSON(w, http.StatusCreated, b)
}

Yangi resurs yaratilganda 201 Created qaytariladi. Location sarlavhasi yangi kitob manzilini bildiradi.

Yangilash: PUT /books/{id}

func (s *server) updateBook(w http.ResponseWriter, r *http.Request) {
    id, err := parseID(r)
    if err != nil {
        writeError(w, http.StatusBadRequest, err.Error())
        return
    }

    in, err := decodeBook(w, r)
    if err != nil {
        writeError(w, http.StatusBadRequest, err.Error())
        return
    }

    b, err := s.store.update(id, in)
    if errors.Is(err, errNotFound) {
        writeError(w, http.StatusNotFound, err.Error())
        return
    }

    writeJSON(w, http.StatusOK, b)
}

PUT resursni to‘liq almashtiradi, shuning uchun barcha maydonlar majburiy. Faqat bitta maydonni o‘zgartirish kerak bo‘lsa, PATCH ishlatiladi. Uni loyihani oddiyroq saqlash uchun bu yerda yozmadik.

O‘chirish: DELETE /books/{id}

func (s *server) deleteBook(w http.ResponseWriter, r *http.Request) {
    id, err := parseID(r)
    if err != nil {
        writeError(w, http.StatusBadRequest, err.Error())
        return
    }

    if err := s.store.delete(id); errors.Is(err, errNotFound) {
        writeError(w, http.StatusNotFound, err.Error())
        return
    }

    w.WriteHeader(http.StatusNoContent)
}

Muvaffaqiyatli o‘chirishdan keyin 204 No Content qaytariladi. Javobda tana bo‘lmaydi.

5. Router va server

func (s *server) routes() http.Handler {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /books", s.listBooks)
    mux.HandleFunc("POST /books", s.createBook)
    mux.HandleFunc("GET /books/{id}", s.getBook)
    mux.HandleFunc("PUT /books/{id}", s.updateBook)
    mux.HandleFunc("DELETE /books/{id}", s.deleteBook)
    return mux
}

s.listBooks — metod qiymati (method value). U s obyektiga bog‘langan oddiy funksiyaga aylanadi va HandleFuncga to‘g‘ridan-to‘g‘ri beriladi.

To‘liq kod

main.go
package main

import (
    "encoding/json"
    "errors"
    "fmt"
    "log"
    "net/http"
    "slices"
    "strconv"
    "strings"
    "sync"
    "time"
)

// ---------- Model ----------

type Book struct {
    ID        int       `json:"id"`
    Title     string    `json:"title"`
    Author    string    `json:"author"`
    Year      int       `json:"year"`
    CreatedAt time.Time `json:"created_at"`
}

type bookInput struct {
    Title  string `json:"title"`
    Author string `json:"author"`
    Year   int    `json:"year"`
}

// ---------- Store ----------

var errNotFound = errors.New("kitob topilmadi")

type store struct {
    mu     sync.RWMutex
    books  map[int]Book
    nextID int
}

func newStore() *store {
    return &store{books: make(map[int]Book), nextID: 1}
}

func (s *store) list() []Book {
    s.mu.RLock()
    defer s.mu.RUnlock()

    books := make([]Book, 0, len(s.books))
    for _, b := range s.books {
        books = append(books, b)
    }
    slices.SortFunc(books, func(a, b Book) int { return a.ID - b.ID })
    return books
}

func (s *store) get(id int) (Book, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()

    b, ok := s.books[id]
    if !ok {
        return Book{}, errNotFound
    }
    return b, nil
}

func (s *store) create(in bookInput) Book {
    s.mu.Lock()
    defer s.mu.Unlock()

    b := Book{
        ID:        s.nextID,
        Title:     in.Title,
        Author:    in.Author,
        Year:      in.Year,
        CreatedAt: time.Now().UTC(),
    }
    s.books[b.ID] = b
    s.nextID++
    return b
}

func (s *store) update(id int, in bookInput) (Book, error) {
    s.mu.Lock()
    defer s.mu.Unlock()

    b, ok := s.books[id]
    if !ok {
        return Book{}, errNotFound
    }
    b.Title, b.Author, b.Year = in.Title, in.Author, in.Year
    s.books[id] = b
    return b, nil
}

func (s *store) delete(id int) error {
    s.mu.Lock()
    defer s.mu.Unlock()

    if _, ok := s.books[id]; !ok {
        return errNotFound
    }
    delete(s.books, id)
    return nil
}

// ---------- Yordamchilar ----------

func writeJSON(w http.ResponseWriter, status int, data any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    if err := json.NewEncoder(w).Encode(data); err != nil {
        log.Printf("JSON yozishda xato: %v", err)
    }
}

func writeError(w http.ResponseWriter, status int, msg string) {
    writeJSON(w, status, map[string]string{"error": msg})
}

func parseID(r *http.Request) (int, error) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil || id <= 0 {
        return 0, errors.New("id musbat butun son bo‘lishi kerak")
    }
    return id, nil
}

func decodeBook(w http.ResponseWriter, r *http.Request) (bookInput, error) {
    r.Body = http.MaxBytesReader(w, r.Body, 1<<20)

    var in bookInput
    dec := json.NewDecoder(r.Body)
    dec.DisallowUnknownFields()
    if err := dec.Decode(&in); err != nil {
        return in, errors.New("JSON noto‘g‘ri: " + err.Error())
    }

    in.Title = strings.TrimSpace(in.Title)
    in.Author = strings.TrimSpace(in.Author)

    switch {
    case in.Title == "":
        return in, errors.New("title majburiy")
    case in.Author == "":
        return in, errors.New("author majburiy")
    case in.Year < 0 || in.Year > time.Now().Year():
        return in, errors.New("year noto‘g‘ri")
    }
    return in, nil
}

// ---------- Handlerlar ----------

type server struct {
    store *store
}

func (s *server) listBooks(w http.ResponseWriter, r *http.Request) {
    writeJSON(w, http.StatusOK, s.store.list())
}

func (s *server) getBook(w http.ResponseWriter, r *http.Request) {
    id, err := parseID(r)
    if err != nil {
        writeError(w, http.StatusBadRequest, err.Error())
        return
    }

    b, err := s.store.get(id)
    if errors.Is(err, errNotFound) {
        writeError(w, http.StatusNotFound, err.Error())
        return
    }

    writeJSON(w, http.StatusOK, b)
}

func (s *server) createBook(w http.ResponseWriter, r *http.Request) {
    in, err := decodeBook(w, r)
    if err != nil {
        writeError(w, http.StatusBadRequest, err.Error())
        return
    }

    b := s.store.create(in)
    w.Header().Set("Location", fmt.Sprintf("/books/%d", b.ID))
    writeJSON(w, http.StatusCreated, b)
}

func (s *server) updateBook(w http.ResponseWriter, r *http.Request) {
    id, err := parseID(r)
    if err != nil {
        writeError(w, http.StatusBadRequest, err.Error())
        return
    }

    in, err := decodeBook(w, r)
    if err != nil {
        writeError(w, http.StatusBadRequest, err.Error())
        return
    }

    b, err := s.store.update(id, in)
    if errors.Is(err, errNotFound) {
        writeError(w, http.StatusNotFound, err.Error())
        return
    }

    writeJSON(w, http.StatusOK, b)
}

func (s *server) deleteBook(w http.ResponseWriter, r *http.Request) {
    id, err := parseID(r)
    if err != nil {
        writeError(w, http.StatusBadRequest, err.Error())
        return
    }

    if err := s.store.delete(id); errors.Is(err, errNotFound) {
        writeError(w, http.StatusNotFound, err.Error())
        return
    }

    w.WriteHeader(http.StatusNoContent)
}

func (s *server) routes() http.Handler {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /books", s.listBooks)
    mux.HandleFunc("POST /books", s.createBook)
    mux.HandleFunc("GET /books/{id}", s.getBook)
    mux.HandleFunc("PUT /books/{id}", s.updateBook)
    mux.HandleFunc("DELETE /books/{id}", s.deleteBook)
    return mux
}

// ---------- main ----------

func main() {
    srv := &server{store: newStore()}

    httpServer := &http.Server{
        Addr:              ":8080",
        Handler:           srv.routes(),
        ReadHeaderTimeout: 5 * time.Second,
        ReadTimeout:       10 * time.Second,
        WriteTimeout:      10 * time.Second,
        IdleTimeout:       60 * time.Second,
    }

    log.Println("Server: http://localhost:8080")
    log.Fatal(httpServer.ListenAndServe())
}

API’ni sinab ko‘rish

Serverni ishga tushiring:

go run main.go

Boshqa terminalda curl bilan har bir amalni tekshiramiz.

Kitob yaratish:

curl -i -X POST http://localhost:8080/books \
  -H "Content-Type: application/json" \
  -d '{"title":"O‘tkan kunlar","author":"Abdulla Qodiriy","year":1926}'
HTTP/1.1 201 Created
Content-Type: application/json
Location: /books/1

{"id":1,"title":"O‘tkan kunlar","author":"Abdulla Qodiriy","year":1926,"created_at":"2026-09-30T10:00:00.123456Z"}

Yana bitta kitob qo‘shamiz:

curl -X POST http://localhost:8080/books \
  -H "Content-Type: application/json" \
  -d '{"title":"Kecha va kunduz","author":"Cho‘lpon","year":1936}'

Ro‘yxatni olish:

curl http://localhost:8080/books
[{"id":1,"title":"O‘tkan kunlar","author":"Abdulla Qodiriy","year":1926,"created_at":"2026-09-30T10:00:00.123456Z"},{"id":2,"title":"Kecha va kunduz","author":"Cho‘lpon","year":1936,"created_at":"2026-09-30T10:00:05.654321Z"}]

Yangilash:

curl -X PUT http://localhost:8080/books/2 \
  -H "Content-Type: application/json" \
  -d '{"title":"Kecha va kunduz","author":"Abdulhamid Cho‘lpon","year":1936}'

O‘chirish:

curl -i -X DELETE http://localhost:8080/books/1
HTTP/1.1 204 No Content

Xato holatlar:

curl http://localhost:8080/books/1
curl http://localhost:8080/books/abc
curl -X POST http://localhost:8080/books -d '{"title":""}'
curl -X POST http://localhost:8080/books -d '{"titel":"Sarob"}'
{"error":"kitob topilmadi"}
{"error":"id musbat butun son bo‘lishi kerak"}
{"error":"title majburiy"}
{"error":"JSON noto‘g‘ri: json: unknown field \"titel\""}

Info

Ma’lumotlar xotirada saqlanadi. Server qayta ishga tushirilsa, barcha kitoblar yo‘qoladi. Buni Ma’lumotlar bazasi bo‘limida PostgreSQL yordamida hal qilamiz.

Nimani yaxshilash mumkin?

Loyiha ishlaydi, lekin real ilova uchun hali ko‘p narsa yetishmaydi. Keyingi darslarda ularni bosqichma-bosqich qo‘shamiz:

  • har bir so‘rovni log qilish va xatolarni bir joyda ushlash — middleware;
  • kodni handler, service, repository qatlamlariga ajratish — clean architecture;
  • tekshiruvlarni validator kutubxonasi bilan yozish va xatolarni yagona formatda qaytarish — JSON bo‘limi;
  • ma’lumotlarni PostgreSQL’da saqlash — ma’lumotlar bazasi bo‘limi;
  • foydalanuvchilarni tanish va ruxsatlarni tekshirish — authentication va authorization.

Xulosa

Bu loyihada:

  • CRUD amallarini REST endpointlariga moslashtirdik;
  • ichki model (Book) va kirish ma’lumotini (bookInput) ajratdik;
  • umumiy ma’lumotni sync.RWMutex bilan himoyaladik;
  • writeJSON, writeError, parseID kabi yordamchilar bilan takrorlanishni kamaytirdik;
  • har bir holat uchun to‘g‘ri status kod qaytardik: 200, 201, 204, 400, 404.

Keyingi darsda middleware tushunchasi bilan tanishamiz va barcha so‘rovlarni log qilishni o‘rganamiz.