Merge pull request #530 from zgordan-vv/ru

Ru
This commit is contained in:
astaxie
2015-09-02 20:52:29 +08:00
5 changed files with 676 additions and 7 deletions

View File

@@ -1,4 +1,4 @@
# 2.2 Фундамент Go
# 2.2 Основы Go
В этом разделе мы научим Вас тому, как определять константы, переменные, относящиеся к элементарным типам данных, а также некоторым приемам программирования на Go.

395
ru/02.6.md Normal file
View File

@@ -0,0 +1,395 @@
# 2.6 Интерфейсы
## Интерфейсы
Интерфейсы - одна из наиболее тончайших черт дизайна Go. После прочтения этого раздела Вы, скорее всего, будете впечатлены тем, как они реализованы в Go.
### Что такое интерфейс
В общих чертах интерфейс - это набор методов, которые используются для того, чтобы определить набор действий.
Например, в предыдущем примере и у Student(студент), и у Employee(работник) есть метод `SayHi()`, но они делают не одно и то же.
Давайте кое-что сделаем. Мы добавим им еще один метод `Sing()`(Петь), а также метод `BorrowMoney()`(Взять взаймы денег) типу Student и метод `SpendSalary()`(Потратить з/плату) типу Employee.
Теперь у Student есть три метода: `SayHi()`, `Sing()` и `BorrowMoney()`, а у Employee есть `SayHi()`, `Sing()` и `SpendSalary()`.
Такая комбинация методов называется интерфейсом и реализуется как Student, так и Employee. Итак, Student и Employee реализуют интерфейс: `SayHi()` и `Sing()`. В то же время Employee не реализует интерфейс: `SayHi()`, `Sing()`, `BorrowMoney()`, а Student не реализует интерфейс: `SayHi()`, `Sing()`, `SpendSalary()`. Это потому, что у Employee нет метода `BorrowMoney()`, а у Student нет метода `SpendSalary()`.
### Тип "Interface"
Интерфейс определяет набор методов, поэтому, если тип реализует эти методы, говорится, что он реализует интерфейс.
type Human struct {
name string
age int
phone string
}
type Student struct {
Human
school string
loan float32
}
type Employee struct {
Human
company string
money float32
}
func (h *Human) SayHi() {
fmt.Printf("Привет, я - %s, мой номер телефона - %s\n", h.name, h.phone)
}
func (h *Human) Sing(lyrics string) {
fmt.Println("Ля ля, ля ля ля, ля ля ля ля ля...", lyrics)
}
func (h *Human) Guzzle(beerStein string) {
fmt.Println("Guzzle Guzzle Guzzle...", beerStein)
}
// Employee перегружает метод SayHi
func (e *Employee) SayHi() {
fmt.Printf("Привет, я - %s, я работаю в %s. Звоните мне по номеру %s\n", e.name,
e.company, e.phone) //Да, можно разбить строку на 2 строки.
}
func (s *Student) BorrowMoney(amount float32) {
s.loan += amount // (снова и снова...)
}
func (e *Employee) SpendSalary(amount float32) {
e.money -= amount
}
// определяем интерфейс
type Men interface {
SayHi()
Sing(lyrics string)
Guzzle(beerStein string)
}
type YoungChap interface {
SayHi()
Sing(song string)
BorrowMoney(amount float32)
}
type ElderlyGent interface {
SayHi()
Sing(song string)
SpendSalary(amount float32)
}
Интерфейс может быть реализован любым типом данных, и один тип может реализовывать несколько интерфейсов одновременно.
Заметьте, что все типы реализуют пустой интерфейс `interface{}`, так как у него нет методов, а все типы изначально также не имеют методов.
### Значение интерфейса
Итак, какие типы значений может принимать интерфейс? Если мы определили переменную типа interface, то значение любого типа, который реализует этот интерфейс, может быть присвоено этой переменной.
Как в примере выше, если мы определили переменную "m" как интерфейс Men, то все значения типа Student, Human или Employee могут быть присвоены переменной "m". Так что у нас может быть срез элементов типа Men, и значение любого типа, реализующего интерфейс Men, может присвоено элементам этого среза. Но имейте в виду, что срез элементов типа interface не ведет себя так же, как срез из элементов других типов.
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human
school string
loan float32
}
type Employee struct {
Human
company string
money float32
}
func (h Human) SayHi() {
fmt.Printf("Привет, я - %s, мой номер телефона - %s\n", h.name, h.phone)
}
func (h Human) Sing(lyrics string) {
fmt.Println("Ля ля ля ля...", lyrics)
}
func (e Employee) SayHi() {
fmt.Printf("Привет, я - %s, я работаю в %s. Звоните мне по номеру %s\n", e.name,
e.company, e.phone) //Да, здесь можно разбить строку на две.
}
// Интерфейс Men реализуется типами Human, Student и Employee
type Men interface {
SayHi()
Sing(lyrics string)
}
func main() {
mike := Student{Human{"Майк", 25, "222-222-XXX"}, "MIT", 0.00}
paul := Student{Human{"Пол", 26, "111-222-XXX"}, "Harvard", 100}
sam := Employee{Human{"Сэм", 36, "444-222-XXX"}, "Golang Inc.", 1000}
Tom := Employee{Human{"Сэм", 36, "444-222-XXX"}, "Things Ltd.", 5000}
// определяем интерфейс i
var i Men
//i может быть Student
i = mike
fmt.Println("Это Майк, студент:")
i.SayHi()
i.Sing("November rain")
//i может быть Employee
i = Tom
fmt.Println("Это Том, работник:")
i.SayHi()
i.Sing("Born to be wild")
// срез из элементов типа Men
fmt.Println("Давайте создадим срез из Men и посмотрим, что получится")
x := make([]Men, 3)
// Эти три элемента относятся к разным типам, но все они реализуют интерфейс Men
x[0], x[1], x[2] = paul, sam, mike
for _, value := range x {
value.SayHi()
}
}
Интерфейс - это набор абстрактных методов, он может быть реализован типами, не являющимися интерфейсами. Поэтому он не может быть реализован самим собой.
### Пустой интерфейс
Пустой интерфейс - это интерфейс, который не содержит методов. Это очен полезно, если мы хотим хранить данные любого типа в одном месте, и это похоже на void* в C.
// Определим a как пустой интерфейс
var a interface{}
var i int = 5
s := "Привет, мир!"
// a может принимать значение любого типа
a = i
a = s
Если функция использует пустой интерфейс в качестве входного аргумента, она может принимать значения любого типа; если функция использует пустой интерфейс в качестве возвращаемого значения, она может возвращать значения любого типа.
### Интерфейсы как аргументы методов
В интерфейсе может быть использована любая переменная. Как мы можем использовать это, чтобы передать переменную любого типа в функцию?
Например, мы много используем fmt.Println, но Вы замечали, что эта команда может принимать в качестве аргумента данные любого типа? Заглянув в исходный код пакета fmt, мы можем найти следующее определение:
type Stringer interface {
String() string
}
Это значит, что любой тип, реализующий интерфейс Stringer, может быть передан в качестве аргумента в fmt.Println. Давайте докажем это:
package main
import (
"fmt"
"strconv"
)
type Human struct {
name string
age int
phone string
}
// Human реализует fmt.Stringer
func (h Human) String() string {
return "Имя:" + h.name + ", Возраст:" + strconv.Itoa(h.age) + " years, Контакт:" + h.phone
}
func main() {
Bob := Human{"Боб", 39, "000-7777-XXX"}
fmt.Println("Этот человек: ", Bob)
}
Возвращаясь к примеру с Box можно обнаружить, что Color также реализует интерфейс Stringer, поэтому у нас есть возможность изменить формат вывода информации. Если не реализовать этот интерфейс, fmt.Println выведет тип на печать в формате по умолчанию.
fmt.Println("Самая большая коробка: ", boxes.BiggestsColor().String())
fmt.Println("Самая большая коробка: ", boxes.BiggestsColor())
Внимание: Если тип реализует интерфейс `error`, fmt вызовет `error()`, поэтому в этом случае Вам не надо реализовывать Stringer.
### Тип переменной в интерфейсе
Если тип переменной реализует интерфейс, то мы знаем, что значение любого типа, реализующего тот же самый интерфейс, может быть присвоено этой переменной. Но как мы узнаем, значение какого изначально типа присвоено этой переменной? Я покажу Вам два способа узнать это.
- Подтверждение по шаблону запятая-ok
В Go существует синтаксис `value, ok := element.(T)`. Так можно проверить, является ли переменная относящейся к указанному типу, где "value" - значение переменной, "ok" - это переменная булева типа, "element" - переменная типа interface и T - тип, который мы хотим подтвердить.
Если element является переменной типа, который мы указали, ok будет равен true, иначе - false.
Чтобы было понятнее, посмотрим на пример:
package main
import (
"fmt"
"strconv"
)
type Element interface{}
type List []Element
type Person struct {
name string
age int
}
func (p Person) String() string {
return "(Имя: " + p.name + " - возраст: " + strconv.Itoa(p.age) + " лет)"
}
func main() {
list := make(List, 3)
list[0] = 1 // целочисленный тип
list[1] = "Привет" // строка
list[2] = Person{"Деннис", 70}
for index, element := range list {
if value, ok := element.(int); ok {
fmt.Printf("list[%d] - это целое число, его значение - %d\n", index, value)
} else if value, ok := element.(string); ok {
fmt.Printf("list[%d] - это строка, его значение - %s\n", index, value)
} else if value, ok := element.(Person); ok {
fmt.Printf("list[%d] - это Person, его значение %s\n", index, value)
} else {
fmt.Printf("list[%d] - это данные какого-то другого типа\n", index)
}
}
}
Пользоваться этим шаблоном довольно-таки просто, но если надо протестировать много типов, лучше воспользоваться `switch`.
- тест с использованием switch
Давайте перепишем наш пример с использованием `switch`.
package main
import (
"fmt"
"strconv"
)
type Element interface{}
type List []Element
type Person struct {
name string
age int
}
func (p Person) String() string {
return "(Имя: " + p.name + " - возраст: " + strconv.Itoa(p.age) + " лет)"
}
func main() {
list := make(List, 3)
list[0] = 1 // целое число
list[1] = "Hello" // строка
list[2] = Person{"Деннис", 70}
for index, element := range list {
switch value := element.(type) {
case int:
fmt.Printf("list[%d] - целое число, его значение - %d\n", index, value)
case string:
fmt.Printf("list[%d] - строка, его значение - %s\n", index, value)
case Person:
fmt.Printf("list[%d] - Person, его значение - %s\n", index, value)
default:
fmt.Println("list[%d] - данные какого-то другого типа", index)
}
}
}
Нужно запомнить, что конструкция `element.(type)` не может быть использована вне тела `switch`, в этом случае надо использовать шаблон `запятая-ok`.
### Встраиваемые интерфейсы
В синтаксисе Go существует множество встроенной логики, такой, например, как анонимные поля в структуре. Неудивительно, что мы можем использовать в качестве анонимных полей и интерфейсы тоже, но называются они `Встроенные интерфейсы`. В этом случае мы следуем тем же правилам, что и в случае со встроенными полями. А точнее, если в интерфейс встроен другой интерфейс, то этот интерфейс будет иметь в себе все методы встроенного интерфейса.
В исходном коде пакета `container/heap` мы можем видеть следующее определение:
type Interface interface {
sort.Interface // встраиваемый sort.Interface
Push(x interface{}) // метод Push для того, чтобы помещать элементы в кучу
Pop() interface{} // метод Pop, который изымает элементы из кучи
}
Мы видим, что `sort.Interface` является встраиваемым интерфейсом, поэтому в Interface неявно присутствуют три метода, содержащиеся внутри `sort.Interface`:
type Interface interface {
// Len - количество элементов в коллекции
Len() int
// Less определяет, надо ли перемещать элемент с индексом i
// перед элементом с индексом j.
Less(i, j int) bool
// Swap меняем местами элементы с индексами i и j.
Swap(i, j int)
}
Другой пример - `io.ReadWriter` из пакета `io`.
// io.ReadWriter
type ReadWriter interface {
Reader
Writer
}
### Рефлексия
Рефлексия в Go используется для определения информации во время выполнения программы. Мы пользумеся пакетом `reflect`, и эта официальная [статья](http://golang.org/doc/articles/laws_of_reflection.html) объясняет, как reflect работает в Go.
В процессе использования reflect задействованы 3 шага. Во-первых, нужно конвертировать интерфейс в типы reflect (reflect.Type или reflect.Value в зависимости от ситуации).
t := reflect.TypeOf(i) // получает мета-данные типа i в переменную t
v := reflect.ValueOf(i) // получает значение типа i в переменную v
После этого мы может конвертировать типы, полученные в результате рефлексии, для того, чтобы получить нужные нам значения.
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("Тип:", v.Type())
fmt.Println("Вид является float64:", v.Kind() == reflect.Float64)
fmt.Println("Значение:", v.Float())
Наконец, если мы хотим изменить значения типов, полученных в результате рефлексии, нам нужно сделать их изменяемыми. Как было обсуждено ранее, есть разница между передачей по ссылке и по значению. Следующий код не скомпилируется:
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1)
Вместо этого для изменения значений типов, полученных в результате рефлексии, нам нужно использовать следующий код:
var x float64 = 3.4
p := reflect.ValueOf(&x)
v := p.Elem()
v.SetFloat(7.1)
Мы здесь обсудили основы рефлексии, однако, чтобы больше понять, Вы должны больше практиковаться.
## Ссылки
- [Содержание](preface.md)
- Предыдущий раздел: [Объектно-ориентированное программирование](02.5.md)
- Следующий раздел: [Многопоточность](02.7.md)

242
ru/02.7.md Normal file
View File

@@ -0,0 +1,242 @@
# Многопоточность
Go называют C 21 века. Я думаю, этому есть две причины: во-первых, Go - простой язык, во-вторых, многопоточность сегодня является горячей темой, а Go поддерживает многопоточность на уровне языка.
## Горутины
Горутины и многопоточность встроены в ядро Go. Они подобны потокам, но работают по-другому. Больше дюжины горутин будут иметь в своей основе только 5-6 потоков. Go также дает в Ваше распоряжение полную поддержку расшаривания памяти в горутинах. Одна горутина обычно использует 4~5 килобайт памяти стека. Поэтому нетрудно запустить тысячи горутин на одном компьютере. Горутины более эффективны, более удобны в использовании и менее ресурсоемки, чем системные потоки.
Горутины в Go запускаются на менеджере потоков во время выполнения. Чтобы создать новую горутину, которая на низлежащем уровне является функцией, используется ключевое слово `go`, ( ***main() - это горутина*** ).
go hello(a, b, c)
Давайте посмотрим на пример:
package main
import (
"fmt"
"runtime"
)
func say(s string) {
for i := 0; i < 5; i++ {
runtime.Gosched()
fmt.Println(s)
}
}
func main() {
go say("Мир") // создает новую горутину
say("Привет") // текущая горутина
}
Вывод:
Привет
Мир
Привет
Мир
Привет
Мир
Привет
Мир
Привет
Мы видим, что реализовывать многопоточность в Go с помощью ключевого слова `go` очень легко. В примере выше две горутины разделяют память, но лучше следовать следующему рецепту: не используйте разделяемые данные для коммуникаций, а используйте коммуникации для того, чтобы разделять данные.
runtime.Gosched() говорит процессору, что нужно исполнить другие горутины и вернуться затем назад.
Для того, чтобы запустить все горутины, планировщик использует только один поток, что означает, что он один реализует многопоточность. Если для задействования преимущества параллельных процессов Вам надо использовать больше ядер процессора, Вам нужно вызвать runtime.GOMAXPROCS(n), чтобы установить количество ядер, которые Вы хотите использовать. Если `n<1`, эта команда ничего не меняет. В будущем эта функция может быть убрана, больше деталей о параллельных процессах и многопоточности смотрите в этой [статье](http://concur.rspace.googlecode.com/hg/talk/concur.html#landing-slide).
## Каналы
Горутины выполняются в одном и том же адресном пространстве, поэтому, когда Вам нужен доступ к разделяемой памяти, Вам нужно организовывать синхронизацию. Как осуществлять коммуникации между различными горутинами? Для этого Go использует очень хороший механизм коммуникаций, называемый `канал`. `Канал` - это как двусторонняя "труба" в шеллах Unix: используйте `канал`, чтобы посылать и принимать данные. Единственный тип данных, используемый в каналах - это тип `channel` и ключевое слово `chan`. Помните, что для того, чтобы создать `канал`, нужно использовать `make`.
ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})
Для того, чтобы посылать и принимать данные, `канал` использует оператор `<-`.
ch <- v // посылаем v в канал ch.
v := <-ch // получаем данные из c, присваиваем их v
Посмотрим еще на примеры:
package main
import "fmt"
func sum(a []int, c chan int) {
total := 0
for _, v := range a {
total += v
}
c <- total // посылаем total в c
}
func main() {
a := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(a[:len(a)/2], c)
go sum(a[len(a)/2:], c)
x, y := <-c, <-c // принимаем из c
fmt.Println(x, y, x + y)
}
Прием и передача данных по умолчанию блокирует канал, и это делает использование синхронных горутин намного легче. Под блокированием я имею в виду то, что горутина не продолжит свое выполнение, если пытается получить данные из пустого канала, например, (`value := <-ch`), пока другие горутины не отправят данные в этот канал. С другой строны, горутина не продожит свое выполнение, пока данные, которые она послала в канал, например (`ch<-5`), не приняты.
## Буферизованные каналы
Выше я говорил о небуферизованных каналах. В Go также есть буферизованные каналы, которые могут хранить больше, чем один элемент. Нарпимер, `ch := make(chan bool, 4)`, здесь мы создали канал, который может содержать 4 булевых элемента. Поэтому в этот канал мы можем послать до 4 булевых элементов, и горутина не заблокриуется, но она заблокируется, когда Вы попытаетесь послать в канал пятый элемент, и ни одна горутина его не примет.
ch := make(chan type, n)
n == 0 ! не буферизованный(блокирует выполнение горутины)
n > 0 ! буферизованный (не заблокирует выполнение, пока не получит n элементов)
Вы можете попробовать запустить следующий код на своем компьютере и поменять какие-нибудь значения:
package main
import "fmt"
func main() {
c := make(chan int, 2) // если изменить 2 на 1, то в процессе выполнения будет вызвана ошибка, но если заменить 2 на 3, то все будет в порядке
c <- 1
c <- 2
fmt.Println(<-c)
fmt.Println(<-c)
}
## Range и Close
Чтобы оперировать данными в буферизованных каналах так же, как в срезах или картах, мы можем использовать range:
package main
import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 1, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x + y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}
`for i := range c` не остановит чтение данных из канала, пока тот не будет закрыт. Чтобы закрыть канал в вышеприведенном примере мы используем ключевое слово `close`. Послать или принять данные из закрытого канала невозможно; чтобы проверить, закрыт ли канал, можно использовать команду `v, ok := <-ch`. Если `ok` вернул false, это означает, что в канале нет данных, и он был закрыт.
Не забывайте закрывать каналы в горутинах, которые посылают данные, а не в тех, которые получают, иначе очень легко получить состояние "panic".
Другое, что надо помнить о каналах - это то, что каналы не подобны файлам. Вам не нужно закрывать их, пока Вы не придете к выводу, что канал больше не нужен, или Вы хотите выйти из цикла range.
## Select
В примерах выше мы использовали только один канал, но что, если мы имеем дело более чем с одним каналом? В Go есть ключевое слово, называемое `select`, используемое для того, чтобы слушать много каналов.
`select` по умолчанию блокирует дальнейшее выполнение и продолжает его лишь только тогда, когда в одном из каналов появляются данные для получения или отправки. Если несколько каналов готовы принять или отправить данные одновременно, select в случайном порядке выбирает, с каким из них работать.
package main
import "fmt"
func fibonacci(c, quit chan int) {
x, y := 1, 1
for {
select {
case c <- x:
x, y = y, x + y
case <-quit:
fmt.Println("Выход")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
У `select` есть также поведение по умолчанию `default`, так же, как и у `switch`. Если ни один их каналов не готов к использованию, исполняется случай по умолчанию (при этом больше не ожидается, пока канал будет готов).
select {
case i := <-c:
// use i
default:
// выполняется, когда c заблокирован
}
## Тайм-аут
Иногда горутина блокируется. Как избежать того, чтобы это заблокировало всю программу? Это просто, можно установить тайм-аут в select:
func main() {
c := make(chan int)
o := make(chan bool)
go func() {
for {
select {
case v := <- c:
println(v)
case <- time.After(5 * time.Second):
println("Тайм-аут")
o <- true
break
}
}
}()
<- o
}
## Runtime и горутины
В пакете `runtime` есть несколько функций для работы с горутинами:
- `runtime.Goexit()`
Прекращает работу текущей горутины, но функции после слова defer будут выполнены в обычном порядке
- `runtime.Gosched()`
Позволяет планировщику выполнить остальные горутины и затем продолжает выполнение
- `runtime.NumCPU() int`
Возвращает количество ядер процессора
- `runtime.NumGoroutine() int`
Возвращает количество горутин
- `runtime.GOMAXPROCS(n int) int`
Устанавливает количество ядер процессора, которые Вы хотите использовать
## Ссылки
- [Содержание](preface.md)
- Предыдущий раздел: [Интерфейсы](02.6.md)
- Следующий раздел: [Итоги раздела](02.8.md)

32
ru/02.8.md Normal file
View File

@@ -0,0 +1,32 @@
# 2.8 Итоги раздела
В этом разделе мы познакомили Вас с 25 ключевыми словами Go. Давайте снова посмотрим на них:
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
- `var` и `const` используются для определения переменных и констант.
- `package` и `import` используются для работы с пакетами.
- `func` используется для определения функций и методов.
- `return` используется для того, чтобы возвращать данные из функций и методов.
- `defer` используется для определения отложенных функций.
- `go` используется для того, чтобы начать выполнение новой горутины.
- `select` используется для того, чтобы переключаться между несколькими каналами для коммуникации с ними.
- `interface` используется для того, чтобы определять интерфейсы.
- `struct` используется для того, чтобы определять пользовательские типы.
- `break`, `case`, `continue`, `for`, `fallthrough`, `else`, `if`, `switch`, `goto` и `default` были представлены в разделе 2.3.
- `chan` - это тип данных "канал", который служит для того, чтобы осуществлять коммуникации между горутинами.
- `type` нужен для того, чтобы определять пользовательские типы.
- `map` используется для того, чтобы определять карты, которые подобны хэш-таблицам в других языках программирования.
- `range` используется для того, чтобы читать данные из `срезов`, `карт` и `каналов`.
Если Вы поняли, как пользоваться этими 25 ключевыми словами, Вы уже изучили многое из Go.
## Ссылки
- [Содержание](preface.md)
- Предыдущий раздел: [Многопоточность](02.7.md)
- Следующий раздел: [Основы Веба](03.0.md)

View File

@@ -4,15 +4,15 @@
- 1.3. [Утилиты Go](01.3.md)
- 1.4. [Инструменты разработки для Go](01.4.md)
- 1.5. [Итоги раздела](01.5.md)
- 2.[Основы Go](02.0.md)
- 2.1. ["Hello, Go"](02.1.md)
- 2.2. [Фундамент Go](02.2.md)
- 2.[Введение в Go](02.0.md)
- 2.1. ["Привет, Go"](02.1.md)
- 2.2. [Основы Go](02.2.md)
- 2.3. [Управляющие конструкции и функции](02.3.md)
- 2.4. [Структуры](02.4.md)
- 2.5. [Объектно-ориентированное программирование](02.5.md)
- 2.6. [interface](02.6.md)
- 2.7. [Concurrency](02.7.md)
- 2.8. [Summary](02.8.md)
- 2.6. [Интерфейсы](02.6.md)
- 2.7. [Многопоточность](02.7.md)
- 2.8. [Итоги раздела](02.8.md)
- 3.[Web foundation](03.0.md)
- 3.1. [Web working principles](03.1.md)
- 3.2. [Build a simple web server](03.2.md)