Go тілі: нөлден өз блогыңызға дейін 35-сабақ (барлығы 50)
Макет: барлық бетке бір жақтау
Go курсының отыз бесінші сабағы. Блогтың жеті үлгісі беттің бір басталуының жеті көшірмесін ұстап отыр. Бүгін бір жақтау қалады: блоктары бар base, pages пен partials қалталары, семантикалық белгілеу — әрі өлшенген тұзақ, соның кесірінен адамдарда барлық бет кенет біреуін көрсетеді.
Не үшін керек
Блогта осы сабаққа дейін не жиналғанын санайық. Жеті үлгі, әрі олардың әрқайсысында өз <!doctype html>, өз <meta charset>, өз стиль сілтемесі және өз логотипі бар. Бір нәрсенің жеті көшірмесі.
Бет екеу болып тұрғанда бұл ұсақ нәрсе сияқты көрінді. Курста әрі қарай кіру, тіркелу, профиль және баптау пайда болады — сонда көшірме он бірге жетеді. Жоғарғы жолақты өзгерткіңіз келсе — он бір жерде өзгертесіз, оның онында дұрыс.
Екінші себеп те бар, ол ыңғайлылық туралы емес. Блогтың беттері әзірге <h1> мен <p>-дан құралған, ал браузер, іздеу жүйесі және экраннан оқитын бағдарлама жоғарғы жолақ қайда, негізгісі қайда, аяғы қайда екенін айтатын белгілеуді күтеді. Бүгін ол да пайда болады.
Бірден тұтас
Жаңа қалта, go mod init sabaq32. Файл енді бірнешеу, әрі олар бұрынғыдай жатпайды:
templates/
base.html жақтау: бәріне біреу
partials/
header.html беттерге ортақ бөліктер
footer.html
pages/
list.html жақтауға салынатын нәрсе
about.html
main.go
templates/base.html
{{ define "base" -}}
<!doctype html>
<html lang="kk">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/static/style.css">
<title>{{ block "title" . }}Менің блогым{{ end }}</title>
</head>
<body>
{{ template "header" . }}
<main>
{{- block "main" . }}{{ end }}
</main>
{{ template "footer" . }}
</body>
</html>
{{- end }}
templates/partials/header.html
{{ define "header" -}}
<header>
<a href="/"><img src="/static/logo.svg" width="40" height="40" alt=""></a>
<nav>
<a href="/">Мақалалар</a>
<a href="/search">Іздеу</a>
<a href="/about">Блог туралы</a>
</nav>
</header>
{{- end }}
templates/partials/footer.html
{{ define "footer" -}}
<footer>
<p>© {{ .Year }} Менің блогым</p>
</footer>
{{- end }}
templates/pages/list.html
{{ define "title" }}Мақалалар — Менің блогым{{ end }}
{{ define "main" }}
<ul>
{{- range .Articles }}
<li><a href="/read/{{ .Slug }}">{{ .Title }}</a> — {{ .Updated }}</li>
{{- end }}
</ul>
{{- end }}
templates/pages/about.html
{{ define "title" }}Блог туралы — Менің блогым{{ end }}
{{ define "main" }}
<article>
<h1>Блог туралы</h1>
<p>Мұнда мен білгенім туралы жазамын.</p>
</article>
{{- end }}
main.go
package main
import (
"embed"
"fmt"
"html/template"
"io/fs"
"log"
"os"
"path"
"time"
)
//go:embed templates
var files embed.FS
// Article — бет көретін күйіндегі мақала.
type Article struct {
Slug string
Title string
UpdatedAt time.Time
}
// months — өйткені Go-да қазақша ай аттары жоқ: time пакеті тек ағылшыншасын
// біледі.
var months = [...]string{"қаңтар", "ақпан", "наурыз", "сәуір", "мамыр", "маусым",
"шілде", "тамыз", "қыркүйек", "қазан", "қараша", "желтоқсан"}
// Updated күнді адам оқитын түрде қайтарады. Пішімдеу үлгіде емес, осында:
// үлгі көрсетеді, есептемейді.
func (a Article) Updated() string {
if a.UpdatedAt.IsZero() {
return "өңделмеген"
}
return fmt.Sprintf("%d %s %d", a.UpdatedAt.Day(),
months[a.UpdatedAt.Month()-1], a.UpdatedAt.Year())
}
// pages әр бетке бір үлгі жиынын жинайды: макет, барлық бөлік және дәл бір
// бет. Әйтпесе аттары бірдей блоктар бірін-бірі басып тастайды.
func pages() map[string]*template.Template {
found, err := fs.Glob(files, "templates/pages/*.html")
if err != nil || len(found) == 0 {
log.Fatal("беттер табылмады: ", err)
}
out := make(map[string]*template.Template, len(found))
for _, page := range found {
name := path.Base(page)
t, err := template.New(name).ParseFS(files,
"templates/base.html", "templates/partials/*.html", page)
if err != nil {
log.Fatal(name, ": ", err)
}
out[name] = t
}
return out
}
func main() {
tpl := pages()
data := map[string]any{
"Year": 2026,
"Articles": []Article{
{Slug: "dala", Title: "Дала туралы", UpdatedAt: time.Date(2026, time.September, 6, 12, 0, 0, 0, time.UTC)},
{Slug: "salem", Title: "Сәлем, әлем"},
},
}
for _, name := range []string{"list.html", "about.html"} {
fmt.Printf("== %s\n", name)
if err := tpl[name].ExecuteTemplate(os.Stdout, "base", data); err != nil {
log.Fatal(err)
}
fmt.Println()
}
}
Шығатыны — екі дайын бет:
== list.html
<!doctype html>
<html lang="kk">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/static/style.css">
<title>Мақалалар — Менің блогым</title>
</head>
<body>
<header>
<a href="/"><img src="/static/logo.svg" width="40" height="40" alt=""></a>
<nav>
<a href="/">Мақалалар</a>
<a href="/search">Іздеу</a>
<a href="/about">Блог туралы</a>
</nav>
</header>
<main>
<ul>
<li><a href="/read/dala">Дала туралы</a> — 6 қыркүйек 2026</li>
<li><a href="/read/salem">Сәлем, әлем</a> — өңделмеген</li>
</ul>
</main>
<footer>
<p>© 2026 Менің блогым</p>
</footer>
</body>
</html>
== about.html
<!doctype html>
<html lang="kk">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/static/style.css">
<title>Блог туралы — Менің блогым</title>
</head>
<body>
<header>
<a href="/"><img src="/static/logo.svg" width="40" height="40" alt=""></a>
<nav>
<a href="/">Мақалалар</a>
<a href="/search">Іздеу</a>
<a href="/about">Блог туралы</a>
</nav>
</header>
<main>
<article>
<h1>Блог туралы</h1>
<p>Мұнда мен білгенім туралы жазамын.</p>
</article>
</main>
<footer>
<p>© 2026 Менің блогым</p>
</footer>
</body>
</html>
Талдау
define мен block: жақтау және оған салынатын нәрсе
{{ define "header" }} бөлікке ат береді. {{ template "header" . }} бөлікті аты бойынша салады, ал нүкте оған бүкіл бетке берілген деректі береді — оны ұмытсаңыз, аяғы жыл туралы білмей қалады.
{{ block "main" . }} — бұл define мен template-тің бір жазуы: «мұнда осындай атты блок болады, ал оны ешкім бермесе, әдепкі мазмұны мынау». base.html-де мұндай блок екеу: қосалқы мәні Менің блогым болатын терезе тақырыбы және бос main.
Бет ешнәрсені түгел салмайды. Ол тек: «менің title-ім мынау, менің main-ім мынау» дейді. Жақтауды base салады.
pages бен partials қалталары
Бөлу әдемілік үшін емес. partials ішінде жақтауға әрқашан әрі аты бойынша салынатын нәрсе жатады: жоғарғы жолақ, аяғы, әрі қарай — мақала карточкасы, іздеу формасы. pages ішінде — жақтауды толтыратын нәрсе, бір бетке бір файл.
Айырмасы келесі бөлімде көрінеді: үлгі жиындары бір бетке біреуден жиналады, әрі partials әр жиынға түседі, ал pages ішінен жиынға дәл бір файл алынады.
Тұзақ: барлық үлгі бір атау кеңістігінде
Макетті бірінші рет жасайтындардың жартысында ол міне осыдан бұзылады.
ParseFS табылған барлық үлгіні бір жиынға салады, ал жиын ішіндегі ат бәріне ортақ. Екі бет, екеуі де {{ define "main" }} деп жариялайды — сонда екіншісі біріншісін үнсіз басып тастайды. Тексерейік:
== барлық бетке бір жиын
-- list сұраймыз:
<!doctype html>
<html lang="kk">
<head>
<title>Тізім</title>
</head>
<body>
<main><p>бұл тізім</p></main>
</body>
</html>
-- about сұраймыз:
<!doctype html>
<html lang="kk">
<head>
<title>Тізім</title>
</head>
<body>
<main><p>бұл тізім</p></main>
</body>
</html>
Біз әртүрлі бет сұрадық, ал бір бетті — екі рет алдық. Қате жоқ, өйткені Go тұрғысынан жаман ештеңе болған жоқ: жиында бір ғана main үлгісі, оқылғандардың соңғысы қалды.
Елестетіп көріңіз. Барлық қалтасында бір жазуы бар жәшік. Салуға қалағаныңша болады, алуға — тек үстіндегісін.
Әр бетке өз үлгі жиыны
Мұны жиынды бет санымен теңестіріп емдейді:
t, err := template.New(name).ParseFS(files,
"templates/base.html", "templates/partials/*.html", page)
Әр жиынға жақтау, барлық ортақ бөлік және бір бет кіреді. Жиын ішіндегі аттар енді соқтығыспайды: жиында main дәл біреу.
Бұл қосылғанда бір рет жиналады, «беттің аты → жиын» картасына. Ары қарай өңдеуші керекті жиынды алып, онда base-ті орындайды. Байқаңыз: беттің аты емес, әрқашан base орындалады — салатын жақтау ғой.
Кез келген үлгідегі қате оқырманда емес, қосылғанда табылады — үлгілер туралы сабақтағы сол ереже: беттің ортасында тосын жағдай алғаннан гөрі, бәрін басында талдаған дұрыс.
Семантика: header, main, footer, nav, article
Шығуда бет енді «жалпы дивтерден» емес, мағынасы бар тегтерден құралғаны көрінеді: <header> — жоғарғы жолақ, <nav> — навигация, <main> — беттегі жалғыз негізгі мазмұн, <article> — өз бетінше тұрған мәтін, <footer> — аяғы.
Бұл әшекей емес. Экраннан оқитын бағдарлама мәзірді айналып өтіп, бірден <main>-ге баруға мүмкіндік береді. Іздеу жүйесі мазмұнды қаптамадан ажыратады. Браузер оқу режимінде <article>-ден басқасының бәрін тастайды.
Ереже қарапайым: беттегі <main> біреу, <h1> де біреу, ал <div>-ті мағынасы бойынша келетін тег болмағанда алады.
Күн — дерекқорға емес, адамға
Осы кезге дейін блог түзету уақытын SQLite қалай берсе, солай көрсетті: 2026-09-06 12:00:00. Оқырманға бұл ештеңе айтпайды.
Пішімдеу үлгіде емес, Go-да тұрады: үлгі көрсетеді, есептемейді. Типтің әдісі — ең қолайлы орын:
func (a Article) Updated() string {
if a.UpdatedAt.IsZero() {
return "өңделмеген"
}
return fmt.Sprintf("%d %s %d", a.UpdatedAt.Day(),
months[a.UpdatedAt.Month()-1], a.UpdatedAt.Year())
}
Үлгі оны өріс сияқты шақырады — {{ .Updated }}, жақшасыз.
Айлар туралы бөлек айту керек. Go-да қазақша немесе орысша ай аттары жоқ: time пакеті тек ағылшыншасын біледі. Тізімді өзіңіз жазуға тура келеді — осы бағдарламадағыдай. Әрі пішімдеудің өзі Go-да YYYY-MM-DD сияқты әріптермен емес, күн үлгісімен жасалады: 02.01.2006. Бұл кездейсоқ сан емес, «екі мың алтыншы жылдың екінші қаңтары» — нені қайда қою керегін жазу тәсілі.
Бос орындар: {{- және -}}
Үлгі әрекеттердің арасында тұрғанның бәрін, соның ішінде жол ауыстыруды да басып шығарады. Содан range әр айналымға бір бос жол қалдырады да, бет жыртық көрінеді.
Жақшаның ішіндегі минус сол жақтағы бос орынды жейді: {{- range .Articles }} өз алдындағы жол ауыстыруды алып тастайды. Жоғарыдағы шығуда тізім тегіс, тесіксіз кетеді — дәл list.html-дегі минустардың арқасында.
Сабақ картасы
Өз сөзіңізбен айтыңыз
Қарамай, дауыстап немесе қағазға жауап беріңіз. Жауаптары — сабақтың соңында.
- Бір жиындағы
mainблогы бар екі бет неге бір нәрсені көрсетеді? - Өңдеуші нені орындайды — беттің үлгісін бе, әлде
base-ті ме, әрі неге? - Күнді неге үлгіде емес, Go-да пішімдейді?
Тапсырма
Міндетті. Блогты макетке көшіріңіз. Жеті үлгінің бәрі pages бен partials бойынша тарайды, ортақ басталуы base.html-де қалады, навигациясы бар жоғарғы жолақ — partials/header.html-де. Жиындар қосылғанда бір рет картаға жиналады. Белгілеу семантикалық болады, ал түзету күні шикі жол емес, әдіс арқылы шығады.
Мұның бәрі step-20 ішінде жасалған — өзіңіз жасағаннан кейін салыстырыңыз.
Қалауыңызша.
- Навигацияда ағымдағы бетті бөлектеңіз. Үлгіге оның атын беру керек болады.
partials/article-card.htmlқосып, оны тізімде де, тег бетінде де қолданыңыз.{{- range }}ішіндегі минустарды алып тастап, беттің бастапқы кодына браузерден қараңыз.- Мұнда аяқтағы жыл деректермен келеді. Оны
Funcsарқылы үлгі функциясы етіңіз: сонда оны әр бетте беріп отырудың қажеті болмайды, әрі аяғы екі мың жиырма жетінші жылы2026деп тұрмайды. step-20-да дәл солай жасалған.
Блогта бұл қайда тұрады
Бұл — блог оқу мысалы болуын қоятын алғашқы сабақ. Бір жақтау, нағыз белгілеу, адамға арналған күн.
Қарыздар. Стиль әлі де жоққа тән, әрі бұл әдейі: курс безендіруге үйретпейді. Навигация оқырманның қай бетте тұрғанын білмейді. Ал беттерде іздеу жүйесіне арналған сипаттама да, әлеуметтік желіге арналған тақырып та жоқ — бұл іске қосу алдындағы тексеру парағы туралы сабақта болады.
Жауаптар
Жауаптарды көрсету
- Себебі
ParseFSбарлық үлгіні бір жиынға салады, ал ондағы аттар ортақ: екіншіmainбіріншісін басып тастайды. Бұл кезде ешқандай қате болмайды — жиында осы атпен бір ғана үлгі қалады. - Әрқашан
base. Бет текtitleменmainблоктарын береді, ал салатын — жақтау; беттің атын орындаудың мағынасы жоқ, онда<html>де,<body>де жоқ. - Себебі үлгі көрсетеді, есептемейді. Типтің әдісі ережені бір жерде ұстайды, оны тестпен тексеруге болады, әрі ол барлық бетте бірдей жұмыс істейді.
Пікірлер (0)
Пікір қалдыру үшін кіріңіз →
Әзірге пікір жоқ. Бірінші болыңыз.