Shanraq.org Shanraq.org
Go-дағы қате мен журнал: не бұзылғанын көру
IT

Go тілі: нөлден өз блогыңызға дейін 25-сабақ (барлығы 50)

Go-дағы қате мен журнал: не бұзылғанын көру

Go курсының жиырма бесінші сабағы. Өңдеушідегі паника серверді емес, жауапты құлатады, ал оқырман мүлде ештеңе алмайды. Бір аралық қабаттағы recover, беттегі және журналдағы өтініш нөмірі, консольге басудың орнына log/slog және жауап кетіп үлгерсе құтқарудың неге кеш екені.

Не үшін керек

Бәрі жұмыс істеп тұрғанда журнал артық болып көрінеді. Ол дәл сол күні керек болады: оқырман «сайтыңыз ашылмайды» деп жазады да, басқа ештеңе айтпайды.

Бүгін екі сұрақты талдаймыз. Бағдарлама бұзылғанда оқырман не көреді. Және себебін түсіну үшін сізде не қалуы керек.

Бірден тұтас

Жаңа қалта, go mod init sabaq23, main.go:

package main

import (
	"errors"
	"fmt"
	"log/slog"
	"math/rand/v2"
	"net/http"
	"os"
	"runtime/debug"
)

var log = slog.New(slog.NewTextHandler(os.Stdout, nil))

// Өтініш нөмірін өзіміз қойған тақырыптан кері оқимыз.
func reqID(w http.ResponseWriter) string { return w.Header().Get("X-Request-Id") }

func withID(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("X-Request-Id", fmt.Sprintf("%08x", rand.Uint32()))
		next.ServeHTTP(w, r)
	})
}

func recoverPanic(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if v := recover(); v != nil {
				w.Header().Set("Connection", "close")
				log.Error("паника", "id", reqID(w), "жол", r.URL.Path,
					"себебі", fmt.Sprint(v), "стек", string(debug.Stack()))
				serverError(w)
			}
		}()
		next.ServeHTTP(w, r)
	})
}

// Барлық оқырманға бір сөйлем, ал егжей-тегжейі журналда қалады.
func serverError(w http.ResponseWriter) {
	w.WriteHeader(http.StatusInternalServerError)
	fmt.Fprintf(w, "Бізде бірдеңе бұзылды. Өтініш нөмірі: %s\n", reqID(w))
}

var errNotFound = errors.New("мақала табылмады")

func find(slug string) (string, error) {
	titles := map[string]string{"dala": "Дала туралы", "shanyraq": "Шаңырақ деген не"}
	if t, ok := titles[slug]; ok {
		return t, nil
	}
	return "", fmt.Errorf("find %q: %w", slug, errNotFound)
}

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Менің блогым")
	})

	mux.HandleFunc("GET /read/{slug}", func(w http.ResponseWriter, r *http.Request) {
		title, err := find(r.PathValue("slug"))
		switch {
		case errors.Is(err, errNotFound):
			log.Info("мақала жоқ", "id", reqID(w), "мекенжай", r.PathValue("slug"))
			http.NotFound(w, r)
			return
		case err != nil:
			log.Error("оқи алмадық", "id", reqID(w), "қате", err)
			serverError(w)
			return
		}
		fmt.Fprintln(w, title)
	})

	mux.HandleFunc("GET /panic", func(w http.ResponseWriter, r *http.Request) {
		var titles map[string]string
		titles["dala"] = "Дала туралы" // нөл-сөздікке жазу
	})

	handler := withID(recoverPanic(mux))

	log.Info("сервер іске қосылды", "мекенжай", "http://localhost:8080")
	if err := http.ListenAndServe(":8080", handler); err != nil {
		log.Error("сервер тоқтады", "қате", err)
		os.Exit(1)
	}
}

Талдау

Паника серверді емес, жауапты құлатады

Әуелі ешқандай қорғанышсыз не болатынын көрейік. Соңғы жолдан recoverPanic-ті бір минутқа алып тастаңыз да, /panic шақырыңыз:

curl -i http://localhost:8080/panic
curl: (52) Empty reply from server

Бес жүзінші емес. Бос бет те емес. Ештеңе: байланыс жабылды, жауап коды мүлде болмады.

Сонда да сервер тірі — / бетін тағы сұраңыз, ол жауап береді. net/http паниканы байланыс деңгейінде өзі ұстайды, әрі оның журналына мынадай нәрсе түседі:

2026/09/05 16:03:35 http: panic serving [::1]:55036: assignment to entry in nil map
goroutine 4 [running]:
net/http.(*conn).serve.func1()

Яғни сізде стек бар, ал оқырманда — бос орын. Оның жағынан бұл «сайт бұзылды» болып көрінеді, әрі ол сізге ештеңе жаза алмайды: хабарлайтын нәрсе жоқ.

Елестетіп көріңіз. Сатушы бір ауыз сөз айтпай талып қалады. Дүкен жұмыс істеп тұр, келесі сатып алушыға қызмет көрсетілді, ал бұл сөре алдында тұр әрі кімге жүгінерін де білмейді.

recover — бүкіл бетке бір аралық қабат

recoverPanic-ті қайтарыңыз да, сол мекенжайды шақырыңыз:

HTTP/1.1 500 Internal Server Error
Connection: close
X-Request-Id: 1a2b3c4d
Date: Sat, 05 Sep 2026 11:03:07 GMT
Content-Length: 78
Content-Type: text/plain; charset=utf-8

Бізде бірдеңе бұзылды. Өтініш нөмірі: 1a2b3c4d

Енді бұл — әңгіме: код та бар, бет те бар, нөмір де бар.

Бұл defer мен recover арқылы істейді. defer өңдеуші аяқталғанда функцияны іске қосады — қалыпты аяқталды ма, панимен бе, маңызды емес. Сондай функцияның ішіндегі recover құлауды тоқтатып, немен паника жасалғанын береді.

defer func() {
	if v := recover(); v != nil {
		...
	}
}()

recover() тек кейінге қалдырылған функцияның ішінде ғана мағыналы. Жай ғана шақырылса, ол nil қайтарады да, ештеңе тоқтатпайды.

Connection: close қоямыз, өйткені жартылай жіберілген жауаптан кейінгі байланыс сенімге лайық емес: браузер жаңасын ашсын.

Ал неге бұл әр өңдеушідегі жол емес, аралық қабат: өңдеуші отыз болады, ал ұмытып кету үшін біреуі жетеді. Орама бүкіл mux-қа бір рет қойылады — аралық қабат сабағындағы тәсіл.

Оқырманға — нөмір, журналға — егжей-тегжей

Оқырманға err.Error() жазғың келеді, бірақ олай істеуге болмайды. Қате мәтіні — сіздің ішкі жағыңыз: кесте аты, файл жолы, кейде сұраныстың бір бөлігі. Бөтен адамға бұл көмектеспейді, ал тесік іздеп жүргенге көмектеседі.

Сондықтан сөйлем бәріне бір, ал егжей-тегжейдің орнына — нөмір:

Бізде бірдеңе бұзылды. Өтініш нөмірі: 1a2b3c4d

Дәл сол нөмір журналда тұр:

level=ERROR msg=паника id=1a2b3c4d жол=/panic себебі="assignment to entry in nil map" стек="goroutine 35 [running..."

Үзінділерден уақыт алынды, ал сұраныс нөмірі үлгімен ауыстырылды: сізде ол өзгеше болады, бірақ екі жолда да бірдей.

Оқырман сегіз таңба жібереді — сіз жолды табасыз. Ол нөмірсіз іздеу «шамамен кеше кешке біреу бірдеңе ашты» дегенге ұқсайды.

Нөмірді X-Request-Id тақырыбына саламыз да, сол жерден оқимыз. Ересек әдіс — оны сұраныс контекстінде тасу, бірақ контекст кейінірек болады; тақырыпты сіз білесіз, әрі оның қосымша пайдасы бар: нөмір жауапта көрініп тұрады.

Тәсілдің шегі: жауап кетіп үлгерсе

Енді recover не істей алмайтыны туралы шындығын айтайық.

Екі жүз жол жазып, содан кейін ғана құлаған өңдеуші мынаны береді:

HTTP/1.1 200 OK
...
жол 199 — буферді толтырамыз
Бізде бірдеңе бұзылды. Өтініш нөмірі: 1a2b3c4d

Жауап коды — 200. 500 емес: net/http буфері толғанда тақырыптар кетіп қалды, енді оларды өзгертуге болмайды. Сонда журналға таныс жол түседі:

superfluous response.WriteHeader call from main.serverError (main.go:41)

Оқырман қалыпты көрінетін бетті көреді, ал соңында — кешірім. Іздеу жүйесі 200 көреді де, соны индекстейді.

Осыдан үлгілер сабағында енгізген ереже шығады, әрі ол енді өзін ақтайды: бетті буферге тұтас жинап, бір бөлікпен береді. Сонда құлау бірінші байтқа дейін болады да, recover үлгереді.

log/slog: іздеуге келетін жолдар

Журналдағы fmt.Println — бұл мәтін. slog — бұл өрістер.

log.Info("мақала жоқ", "id", reqID(w), "мекенжай", r.PathValue("slug"))
level=INFO msg="мақала жоқ" id=1a2b3c4d мекенжай=kokek

«Ат — мән» жұптары хабарламадан кейін тұрады. Айырма жол миллион болғанда шығады: мекенжай=kokek дегенді сүзуге, санауға, топтауға болады, ал «kokek мақаласын таппадық» дегенді тек көзбен оқуға болады.

Өңдеушіні жасау кезінде таңдайды:

slog.New(slog.NewTextHandler(os.Stdout, nil))   // адамға
slog.New(slog.NewJSONHandler(os.Stdout, nil))   // машинаға

Text әзірлеушінің иығынан қарап оқуға келеді, JSON-ды журнал сақтау жүйесі жинайды. Бір жолмен ауыстыруға болады, өйткені қалған кодтың бәрі бұған байланбаған.

Деңгейлер — Debug, Info, Warn, Error — бәрін қатарынан оқымау үшін керек. Әдепкіде Debug басылмайды.

Қате әрдайым 500 емес

/read/{slug} бетіне тағы қараңыз. Жоқ мақала — бұл бұзылу емес:

case errors.Is(err, errNotFound):
	log.Info("мақала жоқ", ...)
	http.NotFound(w, r)

Error емес, Info, әрі 500 емес, 404. Оқырман мекенжайдан жаңылды не мақала алынып тасталды — бағдарлама дұрыс жұмыс істеді.

Табылмаған әр бетке Error жазсаңыз, бір аптадан кейін журналда он мың «қате» болады, олардың арасында нағызы жоғалады. Деңгей — бұл уәде: Error дегеніміз «адам қарауы керек».

Бұл бөліну қателер сабағынан келеді: errors.Is «бұл қандай қате» деген сұраққа жауап береді, ал жауап бойынша код та, жазба деңгейі де таңдалады.

Журналды файлға емес, stdout-қа жазады

slog.NewTextHandler(os.Stdout, nil) — стандартты ағынға шығару, әрі бұл сабақ үшін жеңілдету емес.

Файлға өзі жазатын бағдарлама жол, құқық, өлшем, қысқарту және ескіні өшіру туралы ойлауға міндетті. Мұның бәрін оны іске қосатын нәрсе әлдеқашан біледі: systemd, docker, кез келген хостинг. stdout-қа жазыңыз — өз компьютеріңізде жолдарды терминалда көресіз, ал серверде олар кодқа бір де бір түзетусіз бапталған жерге түседі.

Сабақ картасы

Сабақ картасы: паника ұсталады да, оқырман код пен нөмір алады

Өз сөзіңізбен айтыңыз

Қарамай, дауыстап немесе қағазға жауап беріңіз. Жауаптар — сабақтың соңында.

  1. Өңдеуші панимен құлады, ал recover бар аралық қабат жоқ. Оқырман не көреді?
  2. Оқырманға қате мәтінін неге көрсетуге болмайды?
  3. Табылмаған мақала неге Error емес, Info деңгейімен жазылады?

Тапсырма

Міндетті. Бағдарламаға үлгілер сабағындағы үлгілерді қайтарыңыз да, 500 бетін бөлек үлгі етіп жасаңыз — үстінде өтініш нөмірі болсын. Оны буферге жинаңыз. Паника кезінде дәл 500 келетінін, ал беттегі нөмір журналдағы нөмірмен сәйкес келетінін тексеріңіз.

Қалауыңызша.

  • NewTextHandler-ді NewJSONHandler-ге ауыстырыңыз да, сол жолға қараңыз.
  • Әр сұранысқа бір жол жазатын аралық қабат қосыңыз: әдіс, жол, код және уақыт. Кодты өзіңіз есте сақтауға тура келеді — аралық қабат сабағындағыдай. Оны recoverPanic-тің сыртына қойыңыз: әйтпесе паника оның жанынан өтіп кетеді де, сұраныс туралы жазба мүлде шықпайды.
  • Көп жол жазып, содан кейін паника жасайтын өңдеуші жасаңыз да, жауапта 500 коды жоқ екеніне көз жеткізіңіз.

Блогта бұл қайда тұрады

Блог бұзылғанда үндемей қалудан қалды. Әрі қарай — баптау: мекенжай, порт және жолдар кодқа жазылған күйінде қалмайды.

Қарыздар. Өтініш нөмірі тақырыпта тұр, ал оның дұрыс орны — сұраныс контексті; бұл қатар орындау сабағымен бірге болады. Әр сұранысқа арналған жол әзірге бөлек аралық қабатпен жазылады, ал бәріне біреуі керек. Және журналдағы debug.Stack() — ауыр нәрсе: нағыз сайтта оны кез келген қате үшін емес, тек нағыз паника үшін жазады.

Жауаптар

Жауаптарды көрсету
  1. Ештеңе. Байланыс жауапсыз жабылады: curl Empty reply from server дейді, браузер қолжетімсіздік туралы өз бетін көрсетеді. Жауап коды мүлде болмайды. Сервер сонда да жұмысын жалғастырады, ал стек оның өз журналына түседі — яғни сізде ақпарат бар, ал оқырманда жоқ.
  2. Өйткені қате мәтіні — бағдарламаның ішкі жағы: кесте аттары, жолдар, сұраныс бөліктері. Оқырманға бұл ешқандай көмектеспейді, ал осалдық іздеп жүргенге сервердің қалай құрылғанын айтып береді. Сондықтан оқырманға — ортақ сөйлем мен өтініш нөмірі, ал егжей-тегжейі — журналға.
  3. Өйткені бұл бұзылу емес: бағдарлама дұрыс жұмыс істеді, жай ғана ондай мақала жоқ. Error — адам араласуы керек деген уәде. Әр қате мекенжайды солай белгілесеңіз, нағыз қателер мыңдаған жалғанның арасында батып кетеді.

Дереккөздер

Мәтінде қате не теру қатесі кездессе, бізге айтыңыз

Тапсырманы тексеру

Алдымен VS Code-та шешіп, іске қосыңыз — редактор қатені сол жерде көрсетеді. Дайын шешімді осында қойыңыз. Тексеретін — модель: ол қатені атап көрсетеді, бірақ дайын жауапты бермейді.

Тексеру үшін кіру керек. Кіру

Пікірлер (0)

Әзірге пікір жоқ. Бірінші болыңыз.