Go тілі: нөлден өз блогыңызға дейін 44-сабақ (барлығы 50)
JSON және өз API-іңіз: блогты тек браузермен оқымайды
Go курсының қырық төртінші сабағы. Құрылым тегтері өрістің сырттағы атын және мүлде жасырылатынын шешеді; бөтен JSON-дағы артық өріс сұрамасаңыз үндемейді; типсіз сан float64-ке айналады. Әрі қатесі де JSON болатын өңдеуші жауабы.
Не үшін керек
Блог беттерді береді, әрі бұл оның мазмұны адамға емес, басқаға керек болған күнге дейін дұрыс.
Мобильді қосымшаға сол мақалалар керек, бірақ белгілеусіз. Көрші сайтқа — тақырыптар тізімі. Өзіңізге — бәрі орнында ма деп тексеретін скрипт. Оларға <h1> берудің мағынасы жоқ: олар жауапты бағдарламамен талдайды, әрі оларға бағдарлама түсінетін пішім керек.
Ондай пішім — JSON, әрі Go-да ол стандартты кітапханада жатыр.
Бірден тұтас
Жаңа қалта, go mod init sabaq41:
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"time"
)
// Article — сыртқа шығатын нәрсе. Тегтер өрістің JSON-дағы атын шешеді;
// omitempty бос өрісті алып тастайды, ал дефис өрісті мүлде жасырады.
type Article struct {
Slug string `json:"slug"`
Title string `json:"title"`
Tags []string `json:"tags,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitzero"`
Draft bool `json:"-"`
secret string // кіші әріппен — сыртқа ешқашан шықпайды
}
func main() {
fmt.Println("== сыртқа не шығады")
a := Article{
Slug: "dala", Title: "Дала туралы", Draft: true, secret: "бөтен көзге емес",
UpdatedAt: time.Date(2026, time.September, 6, 12, 0, 0, 0, time.UTC),
}
out, _ := json.MarshalIndent(a, "", " ")
fmt.Println(string(out))
fmt.Println()
fmt.Println("== бос өрістер")
empty, _ := json.Marshal(Article{Slug: "salem", Title: "Сәлем"})
fmt.Println(string(empty))
fmt.Println()
fmt.Println("== сырттан не келеді")
body := `{"slug":"koktem","title":"Көктем","tags":["дала"],"артық":1}`
var got Article
fmt.Println("кәдімгі талдау: ", json.Unmarshal([]byte(body), &got), "->", got.Slug, got.Tags)
strict := json.NewDecoder(strings.NewReader(body))
strict.DisallowUnknownFields()
fmt.Println("қатаң талдау: ", strict.Decode(&Article{}))
fmt.Println()
fmt.Println("== типсіз сан")
var any1 any
json.Unmarshal([]byte(`{"n":7}`), &any1)
n := any1.(map[string]any)["n"]
fmt.Printf("%v — бұл %T\n", n, n)
fmt.Println()
fmt.Println("== өңдеушінің жауабы")
srv := httptest.NewServer(routes())
defer srv.Close()
show(srv.URL + "/api/articles/dala")
show(srv.URL + "/api/articles/joq")
}
func routes() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/articles/{slug}", func(w http.ResponseWriter, r *http.Request) {
if r.PathValue("slug") != "dala" {
// Қате де JSON: API-ді оқитын адам жауапты талдайды, суреті бар
// бетке қарамайды.
writeJSON(w, http.StatusNotFound, map[string]string{"error": "мұндай мақала жоқ"})
return
}
writeJSON(w, http.StatusOK, Article{Slug: "dala", Title: "Дала туралы", Tags: []string{"дала"}})
})
return mux
}
// writeJSON тақырыпты жауап кодынан бұрын жазады: WriteHeader-ден кейін
// тақырыптар оқырманға кетіп қойған, оларды өзгертуге кеш.
func writeJSON(w http.ResponseWriter, code int, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(code)
if err := json.NewEncoder(w).Encode(v); err != nil {
// Тақырып кетіп қойды — журналға жазу ғана қалады.
fmt.Println("жауапты жазу сәтсіз:", err)
}
}
Екінші файлдағы show жауапты басып шығарады. Шығатыны:
== сыртқа не шығады
{
"slug": "dala",
"title": "Дала туралы",
"updated_at": "2026-09-06T12:00:00Z"
}
== бос өрістер
{"slug":"salem","title":"Сәлем"}
== сырттан не келеді
кәдімгі талдау: <nil> -> koktem [дала]
қатаң талдау: json: unknown field "артық"
== типсіз сан
7 — бұл float64
== өңдеушінің жауабы
200 application/json; charset=utf-8 -> {"slug":"dala","title":"Дала туралы","tags":["дала"]}
404 application/json; charset=utf-8 -> {"error":"мұндай мақала жоқ"}
Талдау
Сыртқа тек бас әріппен жазылғаны шығады
Шығудың бірінші блогы — үш өріс, ал құрылымда алтау. Қалғаны қайда кетті?
secret кіші әріппен жазылған, яғни пакеттен тыс көрінбейді — encoding/json үшін де. Бұл — пакеттер туралы сабақтағы сол ереже, әрі мұнда оның жағымды салдары бар: ішкі өрісті сыртқа кездейсоқ беру мүмкін емес.
Draft басқа себеппен жоғалды: оның json:"-" тегі бар, бұл «ешқашан шығарма» дегені. Сыртта білудің қажеті жоқ нәрсені осылай жасырады: нобай, ішкі белгілер, қызметтік өрістер.
Елестетіп көріңіз. Дүкеннің витринасы. Оған қоймадағының бәрін қоймайды, әрі мұны қойма емес, витринаны безендіретін адам шешеді.
Тегтер: ат, omitempty және omitzero
Тег — өрістен кейінгі кері тырнақшадағы жол, әрі ол encoding/json-ға не істеу керегін айтады:
Slug string `json:"slug"`
Tags []string `json:"tags,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitzero"`
Тегсіз өріс Go-дағы атымен шығады — бас әріппен Slug. Сыртта олай қабылданбаған: JSON-да slug немесе updated_at деп жазады, әрі тег атты ортақ түрге келтіреді.
omitempty мәні бос болса, өрісті алып тастайды: бос жол, нөл, бос тілім. Шығудың екінші блогында бұл көрінеді — тегсіз мақалада tags өрісі мүлде жоқ.
time.Time үшін omitempty жұмыс істемейді: бұл құрылым, ал құрылым omitempty мағынасында ешқашан «бос» емес. Осыдан omitzero — ол типтің нөлдік мәніне қарайды, әрі шығуда нөлдік уақыт онымен бірге жоғалды.
Бөтен JSON-ды талдау: артығы үндемейді
Үшінші блок — сырттан келетін нәрсе туралы. Денеде құрылымда жоқ артық өрісі бар, әрі кәдімгі талдау бір ауыз сөз айтқан жоқ: <nil>, мақала талданды.
Солай ойластырылған, әрі бұл қауіпті болғанша ыңғайлы: өріс атындағы қате үнсіз өткізіп жіберілген мәнге айналады. Оқырман title-дың орнына titel жіберді — сіз тақырыпсыз мақала қабылдадыңыз да, бұл туралы білмедіңіз.
Сондықтан бөтен JSON қабылданатын жерде қатаңдықты қосады:
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
Сонда артық өріс қатеге айналады: json: unknown field "артық" — әрі жіберуші өз қатесін бір аптадан кейін емес, бірден біледі.
Типсіз сан float64-ке айналады
Төртінші блок қысқа, бірақ ол үшін сағаттап жөндеумен төлейді.
any-ге (немесе map[string]any-ге) талдасаңыз, сандардың бәрі float64 болады. Мәтінде қалай көрінсе, солай int емес, дәл float64 — JSON-да бөлек бүтін тип жоқ, әрі Go ең жалпысын таңдайды.
Осыдан any-ге талдағандарда interface conversion: interface {} is float64, not int паникасы шығады. Емдеуі қарапайым: құрылымға талдаңыз. Сонда int int болып қалады, ал артық өрістер тип бойынша да түсіп қалады.
Тақырыпты кодтан бұрын қояды
writeJSON үш нәрсені қатаң ретпен істейді:
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(code)
json.NewEncoder(w).Encode(v)
Рет кездейсоқ емес. WriteHeader оқырманға күй жолын және барлық тақырыпты бірден жібереді; одан кейін w.Header().Set ештеңе өзгертпейді — Go оны үнсіз елемейді. Content-Type осылай жоғалады, ал браузер сосын не жіберілгенін болжайды.
Денеде де солай: WriteHeader-сіз w-ге алғашқы жазу 200 дегенді білдіреді. Басқа код керек болса, оны бір байт жазбай тұрып қойыңыз.
Қате де JSON
Шығудың соңғы екі жолы: 200 де, 404 те application/json-мен келді.
Бұл — ұсақ нәрсе емес, ереже. API-ді оқитын адам жауапты бағдарламамен талдайды: қатеге суреті бар HTML-бет ұшып келсе, талдау алғашқы таңбада-ақ құлайды, әрі журналда «мұндай мақала жоқ» емес, «күтпеген <» тұрады.
Сондықтан API-де барлық жауапқа бір пішім. Күй коды не болғанын айтады, дене — егжей-тегжейін: {"error": "мұндай мақала жоқ"}.
Жолдың орнына ағын: Encoder мен Decoder
json.Marshal бүкіл жауапты жадта жинайды, ал json.NewEncoder(w).Encode(v) бірден ағынға жазады. Бір мақалаға айырма жоқ; он мыңдық тізімге — бұл түгел ұстаудың қажеті жоқ ондаған мегабайт.
Талдауда да солай: json.NewDecoder(r.Body).Decode(&v) сұраныс денесінен тікелей оқиды, оны жолға жинамай. Әрі DisallowUnknownFields тек декодерде бар, біз оны сол үшін де аштық.
Шекті ұмытпаңыз: сұраныс денесі сырттан келеді, демек жүктеу туралы сабақтағы http.MaxBytesReader мұнда да керек.
Сабақ картасы
Өз сөзіңізбен айтыңыз
Қарамай, дауыстап немесе қағазға жауап беріңіз. Жауаптары — сабақтың соңында.
- Кіші әріппен жазылған өріс JSON-ға неге түспейді?
- Кәдімгі талдауда өріс атындағы қатеге не болады?
Content-Type-ты негеWriteHeader-ден бұрын қояды?
Тапсырма
Міндетті. Блогқа API қосыңыз: GET /api/articles мақалалар тізімін, GET /api/articles/{slug} біреуін береді. Жауаптар мен қателер — дұрыс тақырыбы бар JSON, ал нобайлар мен ішкі өрістер сыртқа шықпайды.
Мұның бәрі step-29 ішінде жасалған — өзіңіз жазғаннан кейін салыстырыңыз.
Қалауыңызша.
- Мақаланы қатаң декодермен қабылдап, жасалғанының мекенжайымен
201қайтаратынPOST /api/articlesжасаңыз. - Жауапқа
reading_timeөрісін қосыңыз — типтің әдісі JSON-ға түспейді, жауапты құрастырғанда санауға тура келеді. - Өз API-іңізді терминалдан тексеріңіз:
curl -s localhost:8090/api/articles | jq.
Блогта бұл қайда тұрады
Блогта мазмұнын оқудың екінші тәсілі пайда болады — әрі белгілеуге тәуелді емес біріншісі. Осы сәттен бастап бетті деректермен оқитындарды сындырмай қайта жазуға болады.
Қарыздар. API бәрін бірден береді, беттеусіз. Ол ешнәрсемен шектелмеген: бір скрипттен секундына жүз сұранысты ешкім тоқтатпайды. Әрі API арқылы жазу бізде жоқ — онымен оны қалай қорғау керегін шешуге тура келер еді.
Жауаптар
Жауаптарды көрсету
- Себебі
encoding/jsonтек экспортталатын өрістерді көреді — кез келген басқа пакеттегідей ереже. Кіші әріппен жазылған өріс ол үшін жоқ, әрі оны сыртқа кездейсоқ беру мүмкін емес. - Ештеңе: кәдімгі талдау құрылымда жоқ өрістерді үнсіз өткізіп жібереді. Жіберуші тақырып жібердім деп сенеді, ал сіз бос жол аласыз.
DisallowUnknownFieldsемдейді. - Себебі
WriteHeaderкүй мен тақырыптарды бір кесек етіп жібереді. Одан кейін тақырыпты өзгертуге кеш — Go елемейді, ал оқырманContent-Type-сыз жауап алады.
Пікірлер (0)
Пікір қалдыру үшін кіріңіз →
Әзірге пікір жоқ. Бірінші болыңыз.