Як перекваліфікуватись з PHP на Go

Автор або джерело: Ярослав Характерник

Першоджерело

Go junior, PHP, tech, перекваліфікація,

Повна версія

Як перекваліфікуватись з PHP на Go

Підписуйтеся наTelegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Привіт, мене звати Ярослав, вже три роки як перекваліфікувався з PHP на Go і тому можу розповісти, які були складності, що сподобалось в Go і де потрібно писати додатковий код в порівнянні з PHP.

Для довідки про свій досвід: з Delphi 1.5 роки, з PHP 4.5 роки і 3 роки досвіду з Go.

Стаття буде цікава розробникам, які придивляються до Go і хочуть охопить основи мови. Тут будуть порівняння з PHP, а в кінці — список навчальних матеріалів.

Мова програмування Go

Ось так я бачу опис Go в її документації golang.org/doc:

Go — найкраща мова програмування за версією самої мови програмування Go.

А так бачу мову програмування як розробник:

Go — C-подібна мова програмування, має лаконічний синтаксис і всього 25 ключових слів go keywords:

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

18 ключових слів виділив як вже знайомі для PHP-розробників.

У Go коду хороша читабельність, мова лаконічна, код легко писати і підтримувати.

У Go вбудована багатопоточність та є сміттяр (garbage collector), код компілюється в бінарник.

У Go багато стандартних пакетів: http, json, strings, bytes, crypto та інших.

У Go багато стандартних інструментів: для форматування коду, для тестування, для профілювання.

Знайомство з Go на прикладах коду

Якщо ви маєте рішучий намір перекваліфікуватись на Go, то рекомендую ознайомитись з ресурсами, які побудовані на прикладах коду:

  • A Tour of Go Go by Example

    • Go by Example англійською мовою
    • Go за Прикладом українською мовою Programming Idioms cheat sheet

    • PHP, Go

    • JavaScript, Go
    • Python, Go
    • Ruby, Go
    • Java, Go

У кожного навчального ресурса є свої переваги. A Tour of Go — офіціний ресурс, приклади коду доступні для редагування, їх можна запустити та побачити результат, можна ввімкнути підсвічування коду (за замовчуванням чомусь вимкнене).

Go by Example (Go за Прикладом) — приклади коду доступні для редагування, їх можна запустити та побачити результат, а також є підсвітка коду.

Programming Idioms cheat sheet PHP, Go — паралельний список прикладів на PHP, JavaScript, Ruby, Python, Java та Go, як словник.

Перша програма «Привіт, світе!»

package main

import "fmt"

func main() {
    fmt.Println("Привіт, світе!")
}

У мене програма збережена у файлі main.go і легко можу запустити в терміналі як скрипт:

go run main.go


Привіт, світе!

Або скомпілювати і запустити:

# go build -o hello ./examples/phptogo/main.go
# go build -o hello ./examples/phptogo
# go build -o hello main.go
# go build -o hello *.go
# go build -o hello .
go build -o hello


./hello


Привіт, світе!

Як працює перша програма «Привіт, світе!»?

package main

import "fmt"

func main() {
    fmt.Println("Привіт, світе!")
}

Хоч програма і дуже просто виглядає, але містить в собі особливості, притаманні Go.

Точкою входу може бути будь-який файл з розширенням .go* , в якому оголошено пакет main та є функція main**.

Якщо помилитесь, то отримаєте одну з помилок:

go run main.go


go run: cannot run non-main package!


go run main.go


# command-line-arguments
runtime.main_main·f: function main is undeclared in the main package

Розглянемо ключове слово import (знайоме слово з JavaScript та Python).

import "fmt"

Якщо ми захочемо імпортувати ще стандартний пакет math , то вже є варіанти:

import "fmt"
import "math"


import (
    "fmt"
    "math"
)

Груповий варіант імпорту зустрчається частіше.

Далі розглянемо особливості пакетів в Go.

Перша особливість — стандартні пакети в Go написані на Go (на відмінну від PHP та JavaScript), можна перейти і глянути код функції fmt.Println. Щоб перейти до реалізації функції в IDE, тисніть Ctrl+B.

В IDE відобразився зміст файлу /usr/local/go/src/fmt/print.go і очікувана функція Println.

// ...

func Println(a ...interface{}) (n int, err error) {
    return Fprintln(os.Stdout, a...)
}

func Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
    p := newPrinter()
    p.doPrintln(a)
    n, err = w.Write(p.buf)
    p.free()
    return
}

// ...

Друга особливість в Go: пакет — це однойменна папка:

tree /usr/local/go/src/fmt -h


/usr/local/go/src/fmt
├── [ 14K]  doc.go
├── [1.0K]  errors.go
├── [2.4K]  errors_test.go
├── [ 12K]  example_test.go
├── [ 219]  export_test.go
├── [ 57K]  fmt_test.go
├── [ 13K]  format.go
├── [1.5K]  gostringer_example_test.go
├── [ 30K]  print.go
├── [ 32K]  scan.go
├── [ 39K]  scan_test.go
├── [ 551]  stringer_example_test.go
└── [2.1K]  stringer_test.go

0 directories, 13 files

Імортуючи пакет, ми імпортуємо папку з усім функціоналом всередині її файлів (В PHP та JavaScript ми підгружаємо конкретний файл).

Третя особливсть — це видимість, в PHP — це public, protected та private, а в Go видимість визначається через написання першої букви ідентифікатора з великої або малої букви.

Ідентифікатори, які починаються з маленької букви, доступні тільки для використання всередині пакету (аналогія: private).

Ідентифікатори, які починаються з великої букви, доступні усюди (аналогія: public).

Розглянемо public видимість на прикладах стандартних бібліотек.

Стандартна бібліотека math :

// file: /usr/local/go/src/math/const.go
package math

// Mathematical constants.
const (
    E     = 2.71 // https://oeis.org/A001113
    Pi    = 3.14 // https://oeis.org/A000796
    Sqrt2 = 1.41 // https://oeis.org/A002193
)


package main

import (
    "fmt"
    "math"
)

func main() {
    fmt.Println("math.E", math.E)
    fmt.Println("math.Pi", math.Pi)
    fmt.Println("math.Sqrt2", math.Sqrt2)
}

image

package image

import (
    "strconv"
)

// A Point is an X, Y coordinate pair. The axes increase right and down.
type Point struct {
    X, Y int
}

// String returns a string representation of p like "(3,4)".
func (p Point) String() string {
    return "(" + strconv.Itoa(p.X) + "," + strconv.Itoa(p.Y) + ")"
}


package main

import (
    "fmt"
    "image"
)

func main() {
    var point = image.Point{
        X: 1,
        Y: 2,
    }

    fmt.Println("point", point.String())
}

Правила видимості розповсюджуються і на поля структур, в данному випадку X, Y публічні.

Приклади private:

package strconv

func lower(c byte) byte {
    return c | ('x' - 'X')
}


package main

import (
    "fmt"
    "strconv"
)

func main() {
    // not work
    fmt.Println("integer to string", strconv.lower(1))
}


go run ./examples/phptogo/main.go


# command-line-arguments
examples/phptogo/main.go:10:35: cannot refer to unexported name strconv.lower
examples/phptogo/main.go:10:35: undefined: strconv.lower

Звісно, є винятки: прості типи, вбудовані в Go (приклад коду з «A Tour of Go»):

package main

import "fmt"

func main() {
    var i int
    var f float64
    var b bool
    var s string
    fmt.Printf("%v %v %v %q\n", i, f, b, s)
}

Зі звичайного прикладу «Привіт, світе!» ви дізнались, що в Go бібліотеки можна прочитати та зрозуміти, що таке пакет і що таке видимість.

Про ще розповісти далі?

При розробці в PHP я використовував і масиви, і об’єкти, і зараз маю вибрати, яка тема має бути наступна:

  • Структури та методи
  • Array, map та slice

Так виглядає структура:

package main

import (
    "fmt"
)

type Company struct {
    ID          uint32
    Alias       string
    Name        string
    CompanyType string
}

func main() {
    var evriusCompany = Company{
        ID:          1,
        Alias:       "evrius",
        Name:        "Evrius",
        CompanyType: "product",
    }

    fmt.Println(evriusCompany)
}

Так виглядає slice:

package main

import (
    "fmt"
)

func main() {
    var companyTypes = []string{
        "product",
        "startup",
        "outsource",
        "outstaff",
        "other",
    }

    fmt.Println(companyTypes)

    for index, value := range companyTypes {
        fmt.Println(index, value)
    }

    for index := range companyTypes {
        fmt.Println(index)
    }

    for _, value := range companyTypes {
        fmt.Println(value)
    }

    for i := 0; i < len(companyTypes); i++ {
        fmt.Println(i, companyTypes[i])
    }

    fmt.Println(len(companyTypes), cap(companyTypes))
}

Так виглядає map:

package main

import (
    "fmt"
)

func main() {
    var companyTypeCountMap = map[string]int{
        "product":   100,
        "startup":   200,
        "outsource": 300,
        "outstaff":  400,
        "other":     500,
    }

    fmt.Println(companyTypeCountMap)

    for key, value := range companyTypeCountMap {
        fmt.Println(key, value)
    }

    for _, value := range companyTypeCountMap {
        fmt.Println(value)
    }

    for key := range companyTypeCountMap {
        fmt.Println(key)
    }

    fmt.Println(len(companyTypeCountMap))
}

А так виглядає об’єднання всього разом:

(структури як елементи слайсу, структури як ключі та значення мапи)

package main

import (
    "fmt"
)

type (
    CompanyType struct {
        Name string
    }

    Company struct {
        ID          uint32
        Alias       string
        Name        string
        CompanyType string
    }
)

const (
    product   = "product"
    startup   = "startup"
    outsource = "outsource"
    outstaff  = "outstaff"
    other     = "other"
)

var (
    productCompanyType = CompanyType{
        Name: product,
    }
    startupCompanyType   = CompanyType{startup}
    outsourceCompanyType = CompanyType{outsource}
)

func main() {
    var companyTypeMap = map[CompanyType][]Company{
        productCompanyType: []Company{
            Company{
                ID:          1,
                Alias:       "evrius",
                Name:        "Evrius",
                CompanyType: product,
            },
        },
        startupCompanyType: []Company{
            {
                ID:          2,
                Alias:       "startupko",
                Name:        "Startupko",
                CompanyType: startup,
            },
        },
        outsourceCompanyType: {
            {
                ID:          3,
                Alias:       "outsourceko",
                Name:        "Outsourceko",
                CompanyType: outsource,
            },
            {
                ID:          4,
                Alias:       "outsourcenko",
                Name:        "Outsourcenko",
                CompanyType: outsource,
            },
        },
        CompanyType{
            Name: outstaff,
        }: {},
        CompanyType{other}: nil,
    }

    for _, companies := range companyTypeMap {
        for _, company := range companies {
            fmt.Println(company)
        }
    }

    fmt.Println(companyTypeMap)
}

Структури та методи

Структури в Go схожі на структури в C + методи.

Щоб краще зрозуміти методи в Go, почнемо їх поступово додавати:

package main

import (
    "fmt"
)

type Company struct {
    ID          uint32
    Alias       string
    Name        string
    CompanyType string
}

func main() {
    var evriusCompany Company

    evriusCompany.ID = 1
    evriusCompany.Alias = "evrius"
    evriusCompany.Name = "Evrius"
    evriusCompany.CompanyType = "product"

    fmt.Println(evriusCompany)
}


go run main


{1 evrius Evrius product}

Спершу винесемо у функцію, яка встановлює ID:

package main

import (
    "fmt"
)

type Company struct {
    ID uint32
}

func main() {
    var evriusCompany Company

    fmt.Println("main before", evriusCompany)

    companySetID(evriusCompany, 1)

    fmt.Println("main after", evriusCompany)
}

func companySetID(company Company, id uint32) {
    fmt.Println("companySetID before", company)
    company.ID = id
    fmt.Println("companySetID after", company)
}


go run main


main before {0}
companySetID before {0}
companySetID after {1}
main after {0}

Як бачимо, значення в середині функції змінилось на 1, але зовні залишилось 0, тому що у функцію передали копію. Щоб показати це, виведемо адреси:

package main

import (
    "fmt"
)

type Company struct {
    ID uint32
}

func main() {
    var evriusCompany Company

    fmt.Printf("main before %+v,          address %p\n", evriusCompany, &evriusCompany)

    companySetID(evriusCompany, 1)

    fmt.Printf("main before %+v,          address %p\n", evriusCompany, &evriusCompany)
}

func companySetID(company Company, id uint32) {
    fmt.Printf("companySetID before %+v,  address %p\n", company, &company)
    company.ID = id
    fmt.Printf("companySetID after %+v,   address %p\n", company, &company)
}


go run main


main before {ID:0},          address 0xc0000140c0
companySetID before {ID:0},  address 0xc0000140e0
companySetID after {ID:1},   address 0xc0000140e0
main before {ID:0},          address 0xc0000140c0

Змінимо код, щоб явно передавати за вказівником:

package main

import (
    "fmt"
)

type Company struct {
    ID uint32
}

func main() {
    var evriusCompany Company

    fmt.Printf("main before %+v,          address %p\n", evriusCompany, &evriusCompany)

    companySetID(&evriusCompany, 1)

    fmt.Printf("main before %+v,          address %p\n", evriusCompany, &evriusCompany)
}

func companySetID(company *Company, id uint32) {
    fmt.Printf("companySetID before %+v, address %p\n", company, company)
    company.ID = id
    fmt.Printf("companySetID after %+v,  address %p\n", company, company)
}


go run main


main before {ID:0},          address 0xc0000140c0
companySetID before &{ID:0}, address 0xc0000140c0
companySetID after &{ID:1},  address 0xc0000140c0
main before {ID:1},          address 0xc0000140c0

Тепер функція змінює значення і ми це бачимо зовні. Інший варіант — оголосити змінну одразу вказівником:

func main() {
    var evriusCompany = &Company{
        ID: 0,
    }

    fmt.Printf("main before %+v,          address %p\n", evriusCompany, evriusCompany)

    companySetID(evriusCompany, 1)

    fmt.Printf("main before %+v,          address %p\n", evriusCompany, evriusCompany)
}

Тепер перетворимо функцію на метод і так само запустимо:

package main

import (
    "fmt"
)

type Company struct {
    ID uint32
}

func main() {
    var evriusCompany Company

    fmt.Printf("main before %+v,          address %p\n", evriusCompany, &evriusCompany)

    evriusCompany.SetID(1)

    fmt.Printf("main before %+v,          address %p\n", evriusCompany, &evriusCompany)
}

func (company *Company) SetID(id uint32) {
    fmt.Printf("companySetID before %+v, address %p\n", company, company)
    company.ID = id
    fmt.Printf("companySetID after %+v,  address %p\n", company, company)
}

І ось тут вже Go самостійно перетворює

evriusCompany.SetID(1)


(&evriusCompany).SetID(1)

Ось приклад, який ще краще показує, як функція перетворилась у метод:

package main

import "fmt"

type Company struct {
    ID uint32
}

func main() {
    var evriusCompany Company = Company{
        ID: 1,
    }

    fmt.Println("get ID by function", Company.GetID(evriusCompany))
    fmt.Println("get ID by method", evriusCompany.GetID())
}

func (company Company) GetID() uint32 {
    return company.ID
}


go run main


get ID by function 1
get ID by method 1

Методи в Go відрізняються від методів в PHP. У PHP методи оголошуються одразу в класі поруч з полями.

Чому в Go методи оголошені окремо? Бо в Go є ще кодогенерація, яка додає методи до структур в окремих файлах того ж пакету, де оголошенп структура.

Аналогія до методів в Go — це JavaScript, в якому додати метод можна будь-де:

String.prototype.FirstLetter = function () {
    if (this.length === 0) {
        return "";
    }

    return this[0];
}

console.log("Evrius".FirstLetter());


node index.js


E

Коли тільки починав з Go, то в мене були складності з & та * : коли що використовувати. Розібрався:

package main

import "fmt"

type Company struct {
    id   uint32
    name string
}

func main() {
    var evriusCompany Company

    evriusCompany.SetID(1)
    evriusCompany.SetName("Evrius")

    fmt.Println("get id by function", Company.GetID(evriusCompany))
    fmt.Println("get id by method", evriusCompany.GetID())
    fmt.Println("get name by function", (*Company).GetName(&evriusCompany))
    fmt.Println("get name by method", evriusCompany.GetName())

    Company.SomeMethod(evriusCompany)
    evriusCompany.SomeMethod()
}

func (c Company) GetID() uint32 {
    return c.id
}

func (c *Company) SetID(id uint32) {
    c.id = id
}

func (c *Company) GetName() string {
    return c.name
}

func (c *Company) SetName(name string) {
    c.name = name
}

func (Company) SomeMethod() {
    fmt.Println("(Company) SomeMethod()")
}

Якщо у вас будуть такі ж складності, то просто пограйтесь з цим самостійно або підгляньте тут (стаття) або тут (відео).

А ще в Go назву екземпляра структури скорочують до одної букви, як у прикладі вище, хоча варіанти з self та this також зустрічаються у проектах (які переносили з іншим мов, коли це було модно):

func (self *Company) GetName() string {
    return self.name
}

func (self *Company) SetName(name string) {
    self.name = name
}


func (this *Company) GetName() string {
    return this.name
}

func (this *Company) SetName(name string) {
    this.name = name
}

Масиви

Масиви за замовчуванням передаються за значенням, як і структури в прикладі вище.

Масиви можна передавати за вказівником:

package main

import (
    "fmt"
    "strings"
)

func main() {
    var companyTypes = [5]string{
        "product",
        "startup",
        "outsource",
        "outstaff",
        "other",
    }

    fmt.Println("before as lower case", companyTypes)
    fmt.Println("upper copy", toUpperCase(companyTypes))
    fmt.Println("after copy as lower case", companyTypes)
    toUpperCaseByPointer(&companyTypes)
    fmt.Println("after pointer as upper case", companyTypes)
}

func toUpperCase(companyTypes [5]string) [5]string {
    for i := 0; i < 5; i++ {
        companyTypes[i] = strings.ToUpper(companyTypes[i])
    }

    return companyTypes
}

func toUpperCaseByPointer(companyTypes *[5]string) {
    for i := 0; i < 5; i++ {
        companyTypes[i] = strings.ToUpper(companyTypes[i])
    }
}


go run main.go


before as lower case [product startup outsource outstaff other]
upper copy [PRODUCT STARTUP OUTSOURCE OUTSTAFF OTHER]
after copy as lower case [product startup outsource outstaff other]
after pointer as upper case [PRODUCT STARTUP OUTSOURCE OUTSTAFF OTHER]

Slice

Про slice, вони ж зрізи, є багато статей, бо вони часто використовуються в проектах. Аналогія — це динамічний масив.

package main

import "fmt"

func main() {
    var types = [5]string{
        "product",
        "startup",
        "outsource",
        "outstaff",
        "other",
    }

    // full slice
    {
        {
            var typesSlice = types[:]

            for i, companyType := range typesSlice {
                fmt.Println("full slice by [:]", i, companyType)
            }
            fmt.Println()
        }

        {
            var typesSlice = types[0:]

            for i, companyType := range typesSlice {
                fmt.Println("full slice by [0:]", i, companyType)
            }
            fmt.Println()
        }

        {
            var typesSlice = types[:len(types)]

            for i, companyType := range typesSlice {
                fmt.Println("full slice by [:len(types)]", i, companyType)
            }
            fmt.Println()
        }

        {
            var typesSlice = types[0:len(types)]

            for i, companyType := range typesSlice {
                fmt.Println("full slice by [0:len(types)]", i, companyType)
            }
            fmt.Println()
        }
    }

    // part slice
    {
        {
            var typesSlice = types[1:3]

            for i, companyType := range typesSlice {
                fmt.Println("part slice by [1:3]", i, companyType)
            }
            fmt.Println()
        }
    }
}


go run main.go


full slice by [:] 0 product
full slice by [:] 1 startup
full slice by [:] 2 outsource
full slice by [:] 3 outstaff
full slice by [:] 4 other

full slice by [0:] 0 product
full slice by [0:] 1 startup
full slice by [0:] 2 outsource
full slice by [0:] 3 outstaff
full slice by [0:] 4 other

full slice by [:len(types)] 0 product
full slice by [:len(types)] 1 startup
full slice by [:len(types)] 2 outsource
full slice by [:len(types)] 3 outstaff
full slice by [:len(types)] 4 other

full slice by [0:len(types)] 0 product
full slice by [0:len(types)] 1 startup
full slice by [0:len(types)] 2 outsource
full slice by [0:len(types)] 3 outstaff
full slice by [0:len(types)] 4 other

part slice by [1:3] 0 startup
part slice by [1:3] 1 outsource

Як видно з прикладу, під капотом слайсу посилання на масив. Ось ще один приклад, щоб зрозуміти, як працює слайс ( cap вбудована функція — capacity = ємність, len вбудована функція = length).

package main

import "fmt"

func main() {
    var items []int

    for i := 0; i < 5; i++ {
        var before = items

        items = append(items, i)

        fmt.Println("before", i, "len", len(before), cap(before), "cap")
        fmt.Println("after ", i, "len", len(items), cap(items), "cap")
        fmt.Println()
    }
}


go run main.go


before 0 len 0 0 cap
after  0 len 1 1 cap

before 1 len 1 1 cap
after  1 len 2 2 cap

before 2 len 2 2 cap
after  2 len 3 4 cap

before 3 len 3 4 cap
after  3 len 4 4 cap

before 4 len 4 4 cap
after  4 len 5 8 cap

Як показав приклад: під капотом слайсу масив, який збільшується в два рази при досягненні свого розміру. Попередній масив також залишається доступним.

Maps

Вам вже знайомі хештаблиці, в Go вони також є і називаються map.

Розглянемо приклад, як працює мапа:

package main

import "fmt"

func main() {
    // uninitialized map
    // var typeCountMap map[string]int = nil
    // var typeCountMap map[string]int = map[string]int(nil)
    var typeCountMap map[string]int

    var types = []string{
        "product",
        "startup",
        "outsource",
        "outstaff",
        "other",
    }

    for _, companyType := range types {
        {
            var count = typeCountMap[companyType]

            fmt.Println("read uninitialized", companyType, "count", count)
        }

        {
            var count, keyExists = typeCountMap[companyType]

            fmt.Println("read uninitialized", companyType, "count", count, "key exists", keyExists)
        }

        // safe delete from uninitialized map
        {
            delete(typeCountMap, companyType)
        }

        var switchErrorOperation = false
        if switchErrorOperation {
            // panic: assignment to entry in nil map
            typeCountMap[companyType] = 1
        }

        fmt.Println()
    }

    // typeCountMap = make(map[string]int)
    // typeCountMap = make(map[string]int, 0)
    typeCountMap = make(map[string]int, len(types))

    for i, companyType := range types {
        typeCountMap[companyType] = i

        {
            var count = typeCountMap[companyType]

            fmt.Println("read", companyType, "count", count)
        }

        {
            var count, keyExists = typeCountMap[companyType]

            fmt.Println("read", companyType, "count", count, "key exists", keyExists)
        }

        fmt.Println("map len", len(typeCountMap))

        fmt.Println()
    }

    var switchErrorOperation = false
    if switchErrorOperation {
        // invalid argument typeCountMap (type map[string]int) for cap
        // not even started
        // cap(typeCountMap)
    }
}

Як видно з прикладу, з мапи можна читати до ініціалізації.

Ще одна особливість — це порядок виводиту ключів в циклі:

package main

import "fmt"

func main() {
    var typeCountMap = map[string]int{
        "product":   1,
        "startup":   2,
        "outsource": 3,
        "outstaff":  4,
        "other":     5,
    }

    var companyType string
    var count int

    for companyType, count = range typeCountMap {
        fmt.Println(companyType, count)
    }

    fmt.Println()

    fmt.Println("last", companyType, count)
    fmt.Println(companyType == "other", count == 5)
}


go run main.go


other 5
product 1
startup 2
outsource 3
outstaff 4

last outstaff 4
false false

Порядок обходу цикла випадковий, відрізняється від звичного в PHP.

І мапи, і слайси вивчаються легко при перекваліфікації, тому приділив їм мало уваги.

Навіщо пусті структури в Go?

Пусті структури не займають місце в пам’яті і зазвичай використовуються як флаги, ось приклад використання:

package main

import "fmt"

func main() {
    var types = []string{
        "product",
        "startup",
        "outsource",
        "outstaff",
    }

    var typeSet = make(map[string]struct{}, len(types))

    for _, companyType := range types {
        typeSet[companyType] = struct{}{}
    }

    {
        var _, exists = typeSet["product"]

        fmt.Println("is product exists in set", exists)
    }

    {
        var _, exists = typeSet["other"]

        fmt.Println("is other exists in set", exists)
    }
}


go run main.go


is product exists in set true
is other exists in set false

Якщо для вас конструкція struct{}{} виглядає дивно, то перепишу зрозуміліше:

package main

import "fmt"

type empty = struct{}

func main() {
    var types = []string{
        "product",
        "startup",
        "outsource",
        "outstaff",
    }

    var typeSet = make(map[string]empty, len(types))

    for _, companyType := range types {
        typeSet[companyType] = empty{}
    }

    fmt.Println(typeSet)
}

Інтерфейси

Це найцікавіше, офіційне визначення:

An interface type is defined as a set of method signatures.

І одразу до прикладу:

package main

import "fmt"

type Company struct {
    id   uint32
    name string
}

type CompanyReader interface {
    ID() uint32
    Name() string
}

func (c *Company) ID() uint32 {
    return c.id
}

func (c *Company) Name() string {
    return c.name
}

func main() {
    var evriusCompany = &Company{
        id:   1,
        name: "evrius",
    }

    {
        var reader CompanyReader = evriusCompany

        fmt.Println("scope reader, id:", reader.ID())
        fmt.Println("scope reader, name:", reader.Name())
        fmt.Println()
    }

    dump(evriusCompany)
    fmt.Println()

    dumpWithInlineInterfaceID(evriusCompany)
    dumpWithInlineInterfaceName(evriusCompany)
}

func dump(reader CompanyReader) {
    fmt.Println("func reader, id:", reader.ID())
    fmt.Println("func reader, name:", reader.Name())
}

// work, but bad code
func dumpWithInlineInterfaceID(reader interface{ ID() uint32 }) {
    fmt.Println("func inline interface id:", reader.ID())
}

// work, but bad code
func dumpWithInlineInterfaceName(reader interface{ Name() string }) {
    fmt.Println("func inline interface name:", reader.Name())
}


go run main.go


scope reader, id: 1
scope reader, name: evrius

func reader, id: 1
func reader, name: evrius

func inline interface id: 1
func inline interface name: evrius

Якщо у структури є всі методи інтерфейсу, то її можна привести до цього інтерфейсу. Коли починав вчити Go, то вважав концепцію інтерфейсів дуже потужною, але ця концепція зустрічаєть у різних формах у сучасних мовах програмування. Ось чудове годинне відео про Concepts vs Typeclasses vs Traits vs Protocols (YouTube, 9 січня 2021 року).

Приведення типів та підняття інтерфейсу

Інтерфейс містить в собі два значення: type та value , тому навіть передаючи пустий interface{} , можна в коді привести до початковго значення або до іншого інтерфейсу:

package main

import "fmt"

type Company struct {
    id   uint32
    name string
}

type CompanyReader interface {
    ID() uint32
    Name() string
}

func (c *Company) ID() uint32 {
    return c.id
}

func (c *Company) Name() string {
    return c.name
}

func main() {
    var evriusCompany = Company{
        id:   1,
        name: "evrius",
    }

    dump(evriusCompany)
    fmt.Println()
    dump(&evriusCompany)
    fmt.Println()
    dump("evrius")
    fmt.Println()
}

func dump(value interface{}) {
    {
        var companyReader, isCompanyReader = value.(CompanyReader)
        if isCompanyReader {
            fmt.Println("CompanyReader, id:", companyReader.ID())
            fmt.Println("CompanyReader, name:", companyReader.Name())
        }
    }

    {
        var company, isCompany = value.(Company)
        if isCompany {
            fmt.Println("company struct, id:", company.ID())
            fmt.Println("company struct, name:", company.Name())
        }
    }

    {
        var company, isPointerCompany = value.(*Company)
        if isPointerCompany {
            fmt.Println("company pointer, id:", company.ID())
            fmt.Println("company pointer, name:", company.Name())
        }
    }

    {
        var stringValue, isString = value.(string)
        if isString {
            fmt.Println("string value:", stringValue)
        }
    }
}


go run main.go


company struct, id: 1
company struct, name: evrius

CompanyReader, id: 1
CompanyReader, name: evrius
company pointer, id: 1
company pointer, name: evrius

string value: evrius

Code assertion

Якщо в програмі Go є пакети або змінні, які не використовуються, то Go це не подобається, тому використовують символ _.

package main

import (
    "fmt"
)

func main() {
    // work
    // var _, companyName = getCompany()

    // not work
    var companyID, companyName = getCompany()

    fmt.Println(companyName)
}

func getCompany() (uint32, string) {
    return 1, "evrius"
}


go run main.go


# command-line-arguments
main.go:8:6: companyID declared but not used

знак _ використовують для type assertion

package main

type Company struct {
    id   uint32
    name string
}

func (c *Company) ID() uint32 {
    return c.id
}

func (c *Company) Name() string {
    return c.name
}

type CompanyReader interface {
    ID() uint32
    Name() string
}

// type assertion interface
var (
    _ CompanyReader = &Company{}
)

// type assertion interface (alternative)
var (
    _ CompanyReader = (*Company)(nil)
)

func main() {
}

Знак _ використовують для version assertion та щоб залатати генерацію коду:

import (
"fmt"
    proto "github.com/gogo/protobuf/proto"
    io "io"
    math "math"
    math_bits "math/bits"
)

// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf

// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package

Функція init та імпорт пакетів

В Go є механізм ініціалізації пакетів, який викликає всі функції init , наявні в пакеті. Однойменних функцій init може бути багато в пакеті та навіть файлі. Щоб показати, як працює init, в кожному файлі з роширенням *.go додав функцію init, папка має таку структуру:

tree ./examples/phptogo


./examples/phptogo
├── init.go
├── internal
│   └── sum.go
└── main.go

init.go

package main

import "fmt"

func init() {
    fmt.Println("init.go init 1")
}

func init() {
    fmt.Println("init.go init 2")
}

func init() {
    fmt.Println("init.go init 3")
}

main.go

package main

import "fmt"

import (
    "fmt"
    _ "gitlab.com/go-yp/stories/examples/phptogo/internal"
)

func init() {
    fmt.Println("main.go init 1")
}

func init() {
    fmt.Println("main.go init 2")
}

func init() {
    fmt.Println("main.go init 3")
}

func main() {
    fmt.Println("main", "Привіт, світе!")
}

func getStringAndPrint(value string) string {
    fmt.Println("getStringAndPrint", value)

    return value
}

./internal/sum.go :

package internal

import "fmt"

func init() {
    fmt.Println("internal sum.go init")
}

func Sum(a, b int) int {
    return a + b
}



go run ./examples/phptogo


internal sum.go init
getStringAndPrint print on var declared
init.go init 2
init.go init 1
init.go init 3
main.go init 3
main.go init 1
main.go init 2
main Привіт, світе!

Функції init викликаються послідовно в довільному порядку.

Хоч імпортований пакет import _ «gitlab.com/go- yp/stories/examples/phptogo/internal» і не використовується, але його функція init була викликана.

А ось реальний приклад, коли це корисно:

import (
    "database/sql"
    "time"

    _ "github.com/go-sql-driver/mysql"
)

// ...

db, err := sql.Open("mysql", "user:[email protected]/dbname")
if err != nil {
    panic(err)
}
// See "Important settings" section.
db.SetConnMaxLifetime(time.Minute * 3)
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(10)

database/sql
github.com/go-sql-driver/mysql

package mysql

import (
    "database/sql"
    "database/sql/driver"
)

type MySQLDriver struct{}

func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
    // ...
}

func init() {
    sql.Register("mysql", &MySQLDriver{})
}

Import . та alias

Якщо з alias-ом пакету при імпорті все зрозуміло, то імпорт через «.» буде новим, використовується рідко:

package main

import (
    mathalias "math"
    . "fmt"
)

func main() {
    Println("Привіт, світе!", mathalias.E)
}

Доменmathalias.com доступний для купівлі.

JSON

В PHP та JavaScript повертати JSON в API простіше, бо мови динамічно типізовані.

В Go спроба писати в стилі PHP буде виглядати криво через використання map[string]interface{} :

package main

import (
    "encoding/json"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/stats.json", func(w http.ResponseWriter, r *http.Request) {
        // {"status":200,"users":{"online":12345,"total":123456}}
        var response = map[string]interface{}{
            "users": map[string]interface{}{
                "total":  123456,
                "online": 12345,
            },
            "status": http.StatusOK,
        }

        var content []byte
        var marshalErr error

        content, marshalErr = json.Marshal(response)
        if marshalErr != nil {
            w.WriteHeader(http.StatusInternalServerError)

            return
        }

        w.Write(content)
    })

    var err = http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Fatal(err)
    }
}

json.Marshal для серіалізації під капотом використовує рефлексію, розбирає переданий тип, його поля і переводить в JSON. Правильніший варіант — це використовувати теги, альтернатива коментарям в PHP:

package main

import (
    "encoding/json"
    "log"
    "net/http"
)

type StatsResponseUsers struct {
    Total  int `json:"total"`
    Online int `json:"online"`
}

type StatsResponse struct {
    Users  StatsResponseUsers `json:"users"`
    Status int                `json:"status"`
}

func main() {
    http.HandleFunc("/stats.json", func(w http.ResponseWriter, r *http.Request) {
        // {"users":{"total":123456,"online":12345},"status":200}
        var response = StatsResponse{
            Users: StatsResponseUsers{
                Total:  123456,
                Online: 12345,
            },
            Status: http.StatusOK,
        }

        var content []byte
        var marshalErr error

        content, marshalErr = json.Marshal(response)
        if marshalErr != nil {
            w.WriteHeader(http.StatusInternalServerError)

            return
        }

        w.Write(content)
    })

    var err = http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Fatal(err)
    }
}

У Go, використовуючи структури та теги, з JSON-ом можна працювати так само просто, як і в PHP та JavaScript, але через відсутність досвіду зі статичними мовами, зустрічав велосипеди і сам писав їх.

І хоч це звучить просто, але про роботу з JSON-ом в Go зроблю окрему статтю.

Потужна система типів

З попередніх прикладів склалось враження, що доступні тільки два варіанти оголошення типу:

type Company struct {
    name string
    // ... other
}

type CompanyReader interface {
    Name() string
    // ... other
}

У мене було схоже враження, коли вже працював з Go. Розглянемо більше доступних варіантів:

Варіант alias:

import "image"

type Point = image.Point





package main

import (
    "fmt"
    "strings"
)

type StringValue string

func (n StringValue) Source() string {
    return string(n)
}

func (n StringValue) Upper() string {
    return strings.ToLower(string(n))
}

func (n StringValue) Lower() string {
    return strings.ToUpper(string(n))
}

func (n *StringValue) Replace(s string) {
    *n = StringValue(s)
}

func main() {
    var value = StringValue("Go")

    fmt.Println(value.Source())
    fmt.Println(value.Lower())
    fmt.Println(value.Upper())
    fmt.Println()

    value.Replace("Rust")

    fmt.Println(value.Source())
    fmt.Println(value.Lower())
    fmt.Println(value.Upper())
    fmt.Println()
}


go run main.go


Go
GO
go

Rust
RUST
rust





package main

import (
    "fmt"
)

type StringSlice []string

func (s *StringSlice) Add(value string) {
    *s = append(*s, value)
}

func main() {
    var slice = StringSlice{}

    slice.Add("Delphi")
    slice.Add("PHP")
    slice.Add("JavaScript")
    slice.Add("TypeScript")
    slice.Add("Go")
    slice.Add("Rust")

    fmt.Println(slice)
}


go run main.go


[Delphi PHP JavaScript TypeScript Go Rust]

Варіант func:

package main

import (
    "database/sql"
    "database/sql/driver"
)

type DatabaseOpen func(string) (driver.Conn, error)

func (d DatabaseOpen) Open(name string) (driver.Conn, error) {
    var conn, err = d(name)

    return conn, err
}

var (
    _ driver.Driver = (DatabaseOpen)(nil)
)

func init() {
    sql.Register("mocksql", DatabaseOpen(func(name string) (driver.Conn, error) {
        return nil, nil
    }))
}

func main() {
    // NOP
}

Усюди []byte

Якщо в PHP та JavaScript взаємодія з файлами (file_get_contents), мережева взаємодія, та серіалізації (json_encode) повертають строки (string) то в Go усюди слайс байтів:

// json.Marshal
func Marshal(v interface{}) ([]byte, error) {
    // ...
}


// ioutil.ReadFile
func ReadFile(filename string) ([]byte, error) {
    // ...
}

Форматування коду в Go

Щоб в команді уникнути суперечок про code style, є інструменти, які автоматично приводять проект до одного стилю. В PHP для цього є PSR та friendsofphp/php-cs-fixer, у JavaScript є ESLint, а в Go для форматування коду є свій стандартний інструмент go fmt.

Можна запустити для всього коду в проекті або для підпапок:

go fmt ./...
go fmt ./components/... ./models/... ./command/... ./templates/...

Після запуску go fmt виводиться список файлів, які були відформатовані. Зазвичай, файли в Go проекті вже відформатовані і форматування запускають перед комітом, але бувають проекти, де розробники про це забувають і тоді прийшовши в такий проект та запустивши go fmt , отримуємо 100+ файлів. У таких випадках краще форматувати тільки ті файли та папки, з якими працюєш.

Тести в Go

Для тестування в Go є стандартний інструмент go test. Тести пишуться в файлах з суфіксом _test.go. Приклад:

tree ./examples/phptogo/internal


./examples/phptogo/internal/
├── sum.go
└── sum_test.go

0 directories, 2 files


func Sum(a, b int) int {
    return a + b
}


import "testing"

func TestSum(t *testing.T) {
    Sum(1, 2)
}

func BenchmarkSum(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = Sum(i, i)
    }
}

А запуск тестів виглядає так:

go test ./examples/phptogo/internal/...


go test ./examples/phptogo/internal/... -run=Sum


ok      gitlab.com/go-yp/stories/examples/phptogo/internal  0.001s


go test ./examples/phptogo/internal/... -v


go test ./examples/phptogo/internal/... -v -run=Sum


=== RUN   TestSum
--- PASS: TestSum (0.00s)
PASS
ok      gitlab.com/go-yp/stories/examples/phptogo/internal  0.001s





go test ./examples/phptogo/internal/... -v -bench=.


=== RUN   TestSum
--- PASS: TestSum (0.00s)
goos: linux
goarch: amd64
pkg: gitlab.com/go-yp/stories/examples/phptogo/internal
BenchmarkSum
BenchmarkSum        1000000000           0.504 ns/op
PASS
ok      gitlab.com/go-yp/stories/examples/phptogo/internal  0.559s

І хоч у файлах *_test.go функції з великої букви, але при спробі використання з інших пакетів, Go напише про помилку. Тести зручно читати, коли знайомишся з новим проектом, а якщо вони відсутні, то написання тестів допоможе краще розібратись в новому проекті.

Тести мають багато налаштувань, тому цього року про це напишу окрему статтю.

Встановлення та налаштування Go

Зазвичай про встановлення Go пишуть з самого початку статті або книги, але кому воно там цікаво?

Я вже звик, що майже все можна встановити через apt-get, але Go тут пасе задніх (Download and install): треба скачати архів, розпакувати в папку /usr/local/go , потім налаштувати linux environment variables: PATH, GOROOT, GOPATH.

Підемо по інструкції: завантажуємо файл-архів з Go з офіційного сайту та розпоковуємо:

tar -C /usr/local -xzf go1.15.7.linux-amd64.tar.gz

~/.profile

export PATH=$PATH:/usr/local/go/bin


source ~/.profile


go version


go version go1.15.7 linux/amd64

~/.profile

export GOROOT=/usr/local/go
export GOPATH=$HOME/go


source ~/.profile

GOROOT вказує, де знаходиться Go, використовується в IDE для підказок стосовно стандартної бібліотеки Go (для переходу по кліку).

GOROOT також можна використовувати коли потрібні різні версії Go на локальній машині, але зазвичай у кожного проекту свій docker-compose.yaml.

GOPATH потрібний, щоб вказати вашу робочу папку. Раніше я зберігав проекти в ~/develop/projects , і хоч GOPATH вказує на ~/go ,але проекти насправді треба зберігати в папці ~/go/src :

tree ~/go/src/gitlab.com/go-yp -L 1


~/go/src/gitlab.com/go-yp
├── go-face-recognition
├── go-three-redis
├── go-warning-codegeneration
├── proto-vs-json-research
└── stories

Хочу зазначити: і я і мої друзі навозились з GOPATH та GOROOT, коли встановлювали Go.

Налаштування проекту

Переважно проекти розташовані в папках виду ~/go/src/github.com/organization/repository, але бувають і екзотичні розташування ~/go/src/letscook.com.ua/backend.

Створимо і запустимо простий проект:

mkdir -p ~/go/src/letscook.com.ua/backend
cd ~/go/src/letscook.com.ua/backend
touch main.go


package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Привіт, світе!"))
    })

    fmt.Println("http server start on port 8080")
    var err = http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Fatal(err)
    }
}


go run main.go


http server start on port 8080


browse http://localhost:8080/





tree .


.
├── controllers
│   └── hello
│       └── greeting.go
└── main.go


package hello

import "net/http"

const (
    greeting = "Привіт, світе!"
)

func HelloHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte(greeting))
}


package main

import (
    "fmt"
    "letscook.com.ua/backend/controllers/hello"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/", hello.HelloHandler)

    fmt.Println("http server start on port 8080")
    var err = http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Fatal(err)
    }
}

Проект, як і раніше, працює.

Зараз проект використовує лише стандартні пакети, але давайте пограємось і підключемо зовнішній пакет github.com/valyala/fasthttp. Перед цим ініціалізуємо проекти через стандартний інструмент go mod.

go mod init


go: creating new go.mod: module letscook.com.ua/backend

Новий файл go.mod містить інформацію про версію Go та поки пусті залежності:

module letscook.com.ua/backend

go 1.15

Для додання залежностей є go get.

go get github.com/valyala/fasthttp


go: github.com/valyala/fasthttp upgrade => v1.19.0

Після додання залежності оновився файл go.mod (та з’явився go.sum ).

module letscook.com.ua/backend

go 1.15

require github.com/valyala/fasthttp v1.19.0 // indirect

Замінивши net/http на fasthttp , будемо мати:

tree . -I vendor


├── controllers
│   └── hello
│       └── greeting.go
├── go.mod
├── go.sum
└── main.go

2 directories, 4 files


package hello

import (
    "github.com/valyala/fasthttp"
)

const (
    greeting = "Привіт, світе!"
)

func HelloHandler(ctx *fasthttp.RequestCtx) {
    ctx.SetBodyString(greeting)
}


package main

import (
    "fmt"
    "github.com/valyala/fasthttp"
    "letscook.com.ua/backend/controllers/hello"
    "log"
)

func main() {
    fmt.Println("http server start on port 8080")
    var err = fasthttp.ListenAndServe(":8080", hello.HelloHandler)
    if err != nil {
        log.Fatal(err)
    }
}

Щоб IDE почала підказувати, які є методи у fasthttp, треба зберегти vendor:

go mod vendor

Ложка дьогтю

Go статично типізований, а дженерики є лише для вбудованих слайсів та мапи, тому в проекті зустрічаються різні велосипеди:

type Company struct {
    Alias string
    Name  string
}

func ToAliasNameMap(items []Company) map[string]string {
    var length = len(items)

    if length == 0 {
        return nil
    }

    var result = make(map[string]string, length)
    for _, item := range items {
        result[item.Alias] = item.Name
    }

    return result
}

в PHP для цього є array_column, array_values, array_keys та інші.

Випадок, коли тип потрібно вказувати явно:

package main

import "fmt"

func main() {
    var count uint8 = uint8(255)

    // down work, need i := uint8(0)
    for i := 0; i < count; i++ {
        fmt.Println(i)
    }

}

Або робити додаткові рутині перетворення типів:

type Company struct {
    Alias string
    Name  string
}

func main() {
    var (
        company1 = Company{
            Alias: "1",
            Name:  "1",
        }
        company2 = Company{
            Alias: "2",
            Name:  "2",
        }
    )

    // work
    insert(
        company1,
        company2,
    )

    // don't work, will error
    //{
    //  var companies = []Company{company1, company2}
    //
    //  // don't work, will error
    //  insert(companies...)
    //}

    // work
    {
        var companies = []Company{company1, company2}

        var items = make([]interface{}, len(companies))
        for i, company := range companies {
            items[i] = company
        }

        // work
        insert(items...)
    }
}

func insert(items ...interface{}) {
    // insert to database
}

Production ready

У Go є багато сучасних інструментів, які згадував в цій статті, але статті з тематикою «is Go production ready?» все одно зустрічаються. Єдиний серйозний дискомфорт, який був в Go, це менеджер пакунків (npm, composer). Проекти, з якими працював на початку, були без менеджеру пакунків або ж мали окремий bash скрипт зі списком go get anything.

Проекти, які починав зі створення репозиторію, робив з експериментальним менеджером пакунків github.com/golang/dep, який вважається застарілим після релізу go mod у версії
go 1.14 25 лютого 2020 року.

От після релізу go mod розробку на Go вважаю повністю комфортною.

Вакансії з Go

Якщо дивитись DOU тренди, то число вакансії на Go за 3 роки збільшилось в 4 рази (~+300%) Golang, в PHP та Java скромніше (~+50%).

На Djinni 240 відкритих вакансій з Go і всього 42 кандидати

Оскільки спеціалістів з Go менше, ніж вакансій, то є компанії, які розглядають кандидатів з досвідом в інших мовах і бажанням опанувати Go. Так, у Daxx є вакансія Senior Platform/Tools Developer (CloudOps team) на проєкті Exabeam — продукт, який відстежує проникнення хакерів до системи та захищає компанії від викрадення персональних даних. На цю вакансію розглядають як спеціалістів зі знанням Go, так і Java та Python девелоперів, які хочуть перекваліфікуватись.

Епілог

Коли думав писати статтю, то хотів розібрати приклад «Привіт, світе!». У процесі написання вже почав сумніватись, чи варто розбирати такий простий приклад, а потім знайшов розбір для C і сумніви зникли.

Коли на початку 2018 року (3 роки тому) переходив з PHP на Go, то погодився на вакансію Junior Go з відповідною винагородою, звісно погоджуватись на винагороду Junior було помилкою (треба було шукати вакансії Middle Go (яких було мало)), бо вже через півроку отримав офер на Middle Go, після того, як глянули на GitLab-і домашній проект з Go (основна частина якого була написана до працевлаштування).

Тому робіть свої проекти на Go, вакансій вистачає.

А причини переходу з PHP на Go прості: цікавіші проекти (бо на PHP це e-commerce та CRM), підвищення кваліфікації, і звісно вищі винагороди.

Анонс цієї статті робив раніше, двоє написали, що цікаво.

Навчальні матеріали

  • A Tour of Go Go by Example

    • Go by Example англійською мовою
    • Go за Прикладом українською мовою Programming Idioms cheat sheet

    • PHP, Go

    • JavaScript, Go
    • Python, Go
    • Ruby, Go
    • Java, Go
  • Gopher Academy — конференцій

  • Data Structures in Golang Series
  • justforfunc: Programming in Go

Схожі статті:

  • Getting into Go for PHP developers
  • How we have used Go and saved up to $50 000

  • How to avoid Go gotchas — стаття про особливості Go

Детальні уроки від Техносфери (російською мовою)

  • Программирование на Go YouTube ~ 16 годин
  • Разработка веб-сервисов на Go — основы языка Coursera
  • Разработка веб-сервисов на Golang, часть 2 Coursera

А ось мої статті про горутини, які допоможуть вам пройти співбесіду:

  • Синхронізація в Go: горутини, тести, варіанти
  • Синхронізація в Go: використання спільних даних

Code review ваших проектів з Go

Звісно, стаття називається «Як перекваліфікуватись з PHP на Go» і відповідь проста: якщо хочете перекваліфікуватись, то починайте робити свої домашні проекти на Go, при розробці дивіться як реалізовані функції в стандартних пакетах Go strconv.ParseUint , strings.Split , strings.Join.

Можете додавати посилання на свої проекти в цій темі, і тему більше прочитають, і ваш проект покритикують.