Як перекваліфікуватись з PHP на Go
Привіт, мене звати Ярослав, вже три роки як перекваліфікувався з PHP на Go і тому можу розповісти, які були складності, що сподобалось в Go і де потрібно писати додатковий код в порівнянні з PHP.
Для довідки про свій досвід: з Delphi 1.5 роки, з PHP 4.5 роки і 3 роки досвіду з Go.
Стаття буде цікава розробникам, які придивляються до Go і хочуть охопить основи мови. Тут будуть порівняння з PHP, а в кінці — список навчальних матеріалів.
Мова програмування Go
Ось так я бачу опис Go в її документації golang.org/doc:
Go — найкраща мова програмування за версією самої мови програмування Go.
А так бачу мову програмування як розробник:
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 |
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 за Прикладом українською мовою
У кожного навчального ресурса є свої переваги. 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) }
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
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") }
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:password@/dbname") if err != nil { panic(err) } // See "Important settings" section. db.SetConnMaxLifetime(time.Minute * 3) db.SetMaxOpenConns(10) db.SetMaxIdleConns(10)
так в пакеті github.com/go-sql-driver/mysql є init який і робить реєстрацію 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
export PATH=$PATH:/usr/local/go/bin
source ~/.profile
go version
go version go1.15.7 linux/amd64
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 за Прикладом українською мовою
Схожі статті:
Детальні уроки від Техносфери (російською мовою)
- Программирование на Go YouTube ~ 16 годин
- Разработка веб-сервисов на Go — основы языка Coursera
- Разработка веб-сервисов на Golang, часть 2 Coursera
А ось мої статті про горутини, які допоможуть вам пройти співбесіду:
Code review ваших проектів з Go
Звісно, стаття називається «Як перекваліфікуватись з PHP на Go» і відповідь проста: якщо хочете перекваліфікуватись, то починайте робити свої домашні проекти на Go, при розробці дивіться як реалізовані функції в стандартних пакетах Go strconv.ParseUint, strings.Split, strings.Join.
Можете додавати посилання на свої проекти в цій темі, і тему більше прочитають, і ваш проект покритикують.
Найкращі коментарі пропустити