Expert JS React Developers for TUI wanted. Join Ciklum and get a $4000 sign-on bonus!
×Закрыть

Як перекваліфікуватись з 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 за Прикладом) — приклади коду доступні для редагування, їх можна запустити та побачити результат, а також є підсвітка коду.

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:password@/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 є 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
варіант string, int та float:
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
варіант slice:
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
хоч є легенди що GOROOT і GOPATH вже зайві, але також їх збережемо в ~/.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), підвищення кваліфікації, і звісно вищі винагороди.

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

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

YouTube канали:

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

  • How to avoid Go gotchas — стаття про особливості Go
  • Детальні уроки від Техносфери (російською мовою)

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

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

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

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

    👍НравитсяПонравилось34
    В избранноеВ избранном24
    Подписаться на автора
    LinkedIn

    Похожие статьи

    Лучшие комментарии пропустить

    Столько кода в одной статье на доу не было с самого... да никогда столько когда на доу не было! Круто!

    Незалежно від відношення до Go, стаття просто крутезна своєю інформативністю.

    Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
    Ctrl + Enter
    Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
    Ctrl + Enter

    Крута стаття. Багато нового для себе дізнався. Дякую.

    Вендор давно не «необхідність», gopls гарно видає підказки по знайденим модулям (ну принамні в vscode).

    з/и/
    ну нарешті хоч якість беклінки на переклад =)

    Gopls on by default in the VS Code Go extension

    Зараз користуюсь Goland де для підказок треба vendor, але хочу спробувати Vim, бо інколи вношу зміни на staging вручну під час тестування

    Попробуйте использовать go modules вместо обычного go path.

    go mod init github.com/user/reponame

    Тогда вендоринг не понадобится, и проект можно в любой директории

    Автор красава. Великий Респект. Стаття одна з найкращих, що я бачив про Го. Автор просто взяв і показав як користуватися всим гоу-арсеналом.

    до кінця не осилив простиню, автору респект і уважуха!

    переквалифицироваться с php на golang вообще имеет смысл? на go днем с огнем вакансий не отыщешь, в отличие от php

    Вакансій значно більше ніж в 2018 році, в статті про це писав, пошукай в тексті статті: «Вакансії з Go»

    не пали контору, більше goшників менша зарплатня.

    Очікував, що хтось напише про винагороди, але більше гоферів = більше вибір проектів

    В трендах раніше можна було обирати одразу дві категорії для порівняння, плануєте повертати? Чи мало користувались?

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

    Для початківців які вибирають між Java та PHP та Python, або між Android чи iOS було гарне порівняння, звісно і зараз можуть порівняти відкривши дві вкладки, але на одній сторінці було комфортніше

    Можливо про це порівняння мало хто знав, навіть серед тих, хто робить рекламні уроки про вибір спеціалізації в ІТ, якщо їм розповісти про порівняння то стане популярним

    не треба перекваліфіковуватись. нема вакансій (ну ваще).

    особлива гарна категорія вакансій — ми шукаємо людину щоб переписала наш пхп на go.

    Тоже некоторое время изучаю go. Застрял на примере, логику работы которого я не понимаю.

    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    func first(s <-chan int, out chan<- int) {
    	var k int
    	for i := 1; ; {
    		select {
    		case out <- i:
    			fmt.Println(i, " first send ")
    			i++
    		case k = <-s:
    			fmt.Println(k, " first signal ")
    			if k == 20 {
    				close(out)
    				return
    			}
    		}
    	}
    }
    
    func second(s chan<- int, in <-chan int) {
    	for x := range in {
    		if x%2 == 0 {
    			fmt.Println(x, " second signal ")
    			s <- x
    		} else {
    			fmt.Println(x, " second read ")
    		}
    	}
    	fmt.Println("out of second")
    }
    
    func main() {
    	A := make(chan int)
    	signal := make(chan int)
    
    	go first(signal, A)
    	go second(signal, A)
    	time.Sleep(2 * time.Second)
    }
    Собственно это несколько модифицированный стандартный способ закрытия пишущего канала с помощью сигнального канала. Судя по выдаче функции first чтение из канала s ВСЕГДА выполняется раньше, чем запись в канал out. Хотя везде декларируется, что select обрабатывает неблокированные каналы в произвольном порядке. Объясните, может я что-то неверно понимаю, или select действительно отдаёт предпочнение чтению из канала перед записью в канал?

    Такі приклади в реальних проектах дуже рідко зустрічаються.

    Если убрать лишний If во втором case,

    select {
    	case out <- i:
    		i++
    	case k = <-s:
    		close(out)
    		return
    }
    то это СТАНДАРТНЫЙ рекомендуемый способ закрытия передающего канала.
    Хотелось бы получить ответ по существу вопроса

    Є канали без буферу #:

    // ch := make(chan int, 0)
    ch := make(chan int)
    By default, sends and receives block until the other side is ready. This allows goroutines to synchronize without explicit locks or condition variables.

    Є канали з буфером #:

    ch := make(chan int, 100)
    Sends to a buffered channel block only when the buffer is full. Receives block when the buffer is empty.

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

    Переписав ваш приклад, щоб подивитись що буде частіше:

    package main
    
    import (
    	"fmt"
    	"sync"
    	"sync/atomic"
    )
    
    const (
    	readFirst  = 1
    	writeFirst = 2
    )
    
    type counter struct {
    	read  uint32
    	write uint32
    }
    
    type orderCounter = [4]counter
    
    func changeOnce(source, value uint32) uint32 {
    	if source == 0 {
    		return value
    	}
    
    	return source
    }
    
    func selectPriority(buffer int, initReadFirst, selectReadFirst bool) uint32 {
    	// if buffer == 0 then unbuffered channel
    	// if buffer == 1 then buffered channel
    
    	var (
    		forRead  = make(chan struct{}, buffer)
    		forWrite = make(chan struct{}, buffer)
    	)
    
    	// init channels
    	if initReadFirst {
    		go func() {
    			forRead <- struct{}{}
    		}()
    
    		go func() {
    			_ = <-forWrite
    		}()
    	} else {
    		go func() {
    			_ = <-forWrite
    		}()
    
    		go func() {
    			forRead <- struct{}{}
    		}()
    	}
    
    	var state uint32
    
    	for i := 0; i < 2; i++ {
    		if selectReadFirst {
    			select {
    			case <-forRead:
    				state = changeOnce(state, readFirst)
    			case forWrite <- struct{}{}:
    				state = changeOnce(state, writeFirst)
    			}
    		} else {
    			select {
    			case forWrite <- struct{}{}:
    				state = changeOnce(state, writeFirst)
    			case <-forRead:
    				state = changeOnce(state, readFirst)
    			}
    		}
    	}
    
    	return state
    }
    
    func testSelectPrioritySequence(buffer, count int) {
    	var counter orderCounter
    
    	for i := 0; i < count; i++ {
    		var orderType = i % 4
    
    		var state = selectPriority(buffer, orderType&1 == 1, orderType&2 == 2)
    
    		switch state {
    		case readFirst:
    			counter[orderType].read += 1
    		case writeFirst:
    			counter[orderType].write += 1
    		}
    	}
    
    	dump(buffer, counter)
    }
    
    func testSelectPriorityParallel(buffer, count int) {
    	var counter orderCounter
    
    	var wg = new(sync.WaitGroup)
    
    	wg.Add(count)
    
    	for i := 0; i < count; i++ {
    		go func(i int) {
    			var orderType = i % 4
    
    			var state = selectPriority(buffer, orderType&1 == 1, orderType&2 == 2)
    
    			switch state {
    			case readFirst:
    				atomic.AddUint32(&counter[orderType].read, 1)
    			case writeFirst:
    				atomic.AddUint32(&counter[orderType].write, 1)
    			}
    
    			wg.Done()
    		}(i)
    	}
    
    	wg.Wait()
    
    	dump(buffer, counter)
    }
    
    func dump(buffer int, counter orderCounter) {
    	// 0 = orderType&1 == 1 // false
    	// 0 = orderType%2 == 2 // false
    	fmt.Println(fmt.Sprintf("buffer %d; init write first; select write first: read count %d; write count %d", buffer, counter[0].read, counter[0].write))
    	// 1 = orderType&1 == 1 // true
    	// 1 = orderType%2 == 2 // false
    	fmt.Println(fmt.Sprintf("buffer %d; init read first ; select write first: read count %d; write count %d", buffer, counter[1].read, counter[1].write))
    	// 2 = orderType&1 == 0 // false
    	// 2 = orderType%2 == 2 // true
    	fmt.Println(fmt.Sprintf("buffer %d; init write first; select read first : read count %d; write count %d", buffer, counter[2].read, counter[2].write))
    	// 3 = orderType&1 == 1 // true
    	// 3 = orderType%2 == 2 // true
    	fmt.Println(fmt.Sprintf("buffer %d; init read first ; select read first : read count %d; write count %d", buffer, counter[3].read, counter[3].write))
    
    	fmt.Println()
    }
    
    func main() {
    	fmt.Println("sequence")
    	testSelectPrioritySequence(0, 8096)
    	testSelectPrioritySequence(1, 8096)
    	fmt.Println("parallel")
    	testSelectPriorityParallel(0, 8096)
    	testSelectPriorityParallel(1, 8096)
    }
    sequence
    buffer 0; init write first; select write first: read count 2023; write count 1
    buffer 0; init read first ; select write first: read count 1; write count 2023
    buffer 0; init write first; select read first : read count 2020; write count 4
    buffer 0; init read first ; select read first : read count 1; write count 2023
    
    buffer 1; init write first; select write first: read count 0; write count 2024
    buffer 1; init read first ; select write first: read count 0; write count 2024
    buffer 1; init write first; select read first : read count 0; write count 2024
    buffer 1; init read first ; select read first : read count 0; write count 2024
    
    parallel
    buffer 0; init write first; select write first: read count 1994; write count 30
    buffer 0; init read first ; select write first: read count 20; write count 2004
    buffer 0; init write first; select read first : read count 1994; write count 30
    buffer 0; init read first ; select read first : read count 17; write count 2007
    
    buffer 1; init write first; select write first: read count 0; write count 2024
    buffer 1; init read first ; select write first: read count 2; write count 2022
    buffer 1; init write first; select read first : read count 0; write count 2024
    buffer 1; init read first ; select read first : read count 0; write count 2024

    Ярослав, ваш тест не эквивалентен моему примеру из-за

    for i := 0; i < 2; i++
     в func selectPriority. Доработал свой пример, и прогнал его в цикле 10000 раз. И все 10000 сработало чтение из канала. Из чего делаю вывод — при работе с небуферизованными каналами select предпочитает чтение из канала записи в канал. То есть в
    *PLACEHOLDERS_PRE_3*ВСЕГДА срабатывает второй case

    Другий case завжди може спрацьовувати саме на вашій версії Go (моя локальна версія Go 1.14), така поведінка може змінитись у майбутніх версіях, і тому автори Go пишуть довільний порядок.

    То вам вдалось розібратись з вашим прикладом, стало зрозуміліше?

    "

    стало зрозуміліше?

    " — Навпаки. Не розумію, або я десь помиляюсь, або на всіх ресурсах по Go надають недостовірну інформацію щодо логіки роботи select, у що також мало віриться

    якщо я вірно зрозумів, що мова саме про select

    golangbot.com/select
    tour.golang.org/concurrency/5

    If multiple operations are ready, one of them is chosen at random

    Так, мова саме про це. Розглядається такий випадок

    select {
    	case out <- i:
    		i++
    	case <-s:
    		close(out)
    		return
    }
    Обидва канали небуферизовані. При 10000 спроб, коли обидва канали неблоковані, кожного разу обирався другий case, читання с каналу

    При 10000 спроб, коли обидва канали неблоковані, кожного разу обирався другий case, читання с каналу

    ну, так, поточна версія робить однаково. Але стандарт не гарантує.

    Просто для ілюстрації: в джаваскрипті таке саме з перебором ключів в об’єктах(хеш-мапах). Більшість движків перебирають в порядку додавання ключів, але це ніде не гарантується, тож краще мати явний ордерінг. Хоча це зайвий код, а актуальні версії браузерів роблять однаково.

    Тобто це

    If multiple operations are ready, one of them is chosen at random

    лише декларується, а реальна реалізація може відрізнятися? Тобто в різних версіях поведінка може бути різною?

    Ярослав, Євген, дякую за роз’яснення

    вопрос опытным. зачем нужен Го при живых сях, плюсах и расте?

    Чтобы дать легкие в написании корутины/фибры

    Это основное, что в других языках сложно, в С++ только вот затянули без батареек. В С давно делались акторы, но все руками. И С и С++ многословные.

    Го дает из коробки хорошую масштабируемость по одновременным запросам, асинхронность, каналы сообщений. При этом его смогли раскрутить лучше, чем Эрланг — частично потому, что Эрланг попытался в функциональщину, а Го хотят оставить простым.

    en.wikipedia.org/...​:_goroutines_and_channels

    понял, спасибо. а в сравнении с растом смодете подбросить хорошее критическое мнение?

    Нет. По расту тут народу хватает — даже тему отдельную завели.
    Насколько я понимаю, Раст — очень злая функциональщина, то есть — идет туда же, где Эрланг. + вообще не знаю, что там с многопоточностью. Возможно, нет ручного управления.
    В результате, код на Го будет на порядки проще читать и писать, чем на Расте. Но на Расте если код уже скомпилился — то, скорее всего, не будет падать. Но это не значит, что программист не допустил логических ошибок, и что код будет работать так, как хочет заказчик или тестировщик.

    Но на Расте если код уже скомпилился — то, скорее всего, не будет падать.

    раст шёл как замена плюсам на уровне предиката «компиляция гарантирует корректность выполнения» что должно было повлиять на возможность привлечения 100500 хомячков педалить код сразу в продакшен без привлечения сложных методоложик тестирования и отладки и поиска утечек вот это всё в т.ч. в вопросах многопоточности «кто кого блочит»

    В результате, код на Го будет на порядки проще читать и писать, чем на Расте.

    ... оказалось что стоимость привлечения 100500 хомячков существенно выше стоимости «традиционных методов» и проект пока застрял на уровне «стильно модно молодёжно но уже вчерашний день» ))

    Насколько я понимаю, Раст — очень злая функциональщина, то есть — идет туда же, где Эрланг.

    це — неправда

    Раст — очень злая

    боровчекр там злий, а не ФункціАнальщина

    Скорость разработки намного выше чем на сях,плюсах и расте

    Сі занадто лоу-левельний, можно сказать на сходинку вище від асемблеру.
    С++ переускладнене непорозуміння
    Rust — спроба написати C++ правильно. Все ще досить складний для вивчення. Треба паритись про пам’ять самостійно, задовільняючи borrow checker.
    Go — засвоїв за пару вечорів і погнав педалить код. Базу даних, переганялку байтів, інтернет магазин — пиши що захочеш, і ніхто тебе не задовбуватиме теорією категорій та менеджментом пам’яті. Все працюватиме з нативною швидкістю, не віджиратиме пам’ять як джава, повноцінні корутини з коробки, код статично типізовавний спеціально спрощений для простоти читання.
    Скоро ще дженерики завезуть, а там і від err != nil можна спекатись.
    Явважаю це доволі вагомі переваги.

    Як у нього з великими проектами (100+ тисяч рядків) без ООП? Щось мені здається, що буде програвати С++.

    Там нема джава-стайл ООП, і це добре. Місцевий аналог навпаки дозволяє будувати великі проекти без фабрик стратегій візиторів.

    На чому місцевий аналог базується?
    С++ віджерло в С усі великі проекти, крім ядер ОС, саме завдяки нативному ООП.

    Не ООП, а скоріше хоч якійсь можливості робити foo.bar(100) замість foo(bar, 100).
    В го можна також визначати щось типу «методів» для типу. Нема наслідування, є анонімні вкладення які працюють схожим чином: розширена структура отримує всі методи базового типу і нібито проксіює всі їх виклики в базову. Для того щоб тип задовільняв інтерфейсу, достатньо щоб він реалізував методи з такими ж сігнатурами як і в інтерфейсі. Це дозволяє перейти до пласких ієрархій: от є моя функція, що закриває речі. Якщо ви хочете щоб вона закривала вашу річ, реалізуйте на ній метод `Close() error` і просто передайте значення цього типу. Не треба ніяких явних implements як в жаві. Можна навіть навпаки створювати інтерфейси під існуючі типи з сторонніх бібліотек.

    Почитал, как template method делают. Прикольно. Вроде, все должно работать. И дженерики собираются подвезти.

    да, Пайк перегнул палку — дженерики в Go все же нужны.

    Так здається всі з цього погоджувались з самого початку.
    Тільки ніхто не міг придумать як їх грамотніше організувати.

    Жалко що для помилок нормального функціоналу не завезли. А то могли б напрягтись і зробити вбудований тип result наприклад. І нормальний синтаксичний цукор щоб не копіпастить ерр != ніл.

    А то могли б напрягтись і зробити вбудований тип result наприклад. І нормальний синтаксичний цукор щоб не копіпастить ерр != ніл.

    Вот-вот, тем более, что в том же Rust это есть.

    простіше від С++ і Rust, але висоуровніше від C
    рекомендую

    Блин, хотел добавить код. Теги «code» вставил, они не работают. Как же вставить код, кто подскажет?

    Відкриваєш код сторінки через Ctrl+U, дивишся блок коду, додаєш коментар і перезавантажуєш сторінку

    <div class="hl-wrap golang"><pre>
    </pre></div>
    
    <div class="hl-wrap bash"><pre>
    </pre></div>
    
    <div class="hl-wrap text"><pre>
    </pre></div>
    

    Так все-таки, хтось може пояснити, які проекти пишуть на Го? Бази даних / високонагружені бекенди / веб? В яких доменах (фінанси / реклама / ?).
    Якось робив тестове завдання на Го, зовсім його не знаючи — один вечір почитав офіційний туторіал по синтаксису, потім доку по echo / gorm / viper / goose / dep etc і написав рест круд із аутентифікацією, було ясно нескладно, але дуже незвично після джави, особливо із управлінням пакетами якась дічь (або я не розібрався нормально).
    P. S. Стаття крута однозначно

    Все ж того що згадав пишуть на Go, є навіть версія Redis на Go — LedisDB

    Як і писав в production ready: менеджер пакетів виправили

    Це може бути невиправна ментальна деформація/травма від попереднього досвіду... ;))

    Мде, замість того щоб просто відповісти на питання, треба обов’язково якось з’язвити і під*бнути, воістину український (пост-совковий) менталітет «неіскорєнім»

    Нууу.. щодо відповіді на питання, то мені не вистачає наснаги та бажання щодо цього... тут є більш завзяті та скіловані, які допоможуть, за що їм велика подяка!!
    Я ж намагався з’ясувати причини такої скадності в розумінні... спостерігаю «головний біль» щодо Go саме у адептів JVM у більшості випадків... от і виказав таке припущення...
    пс Гадаю, що жоден гофер не намагатиметься добровільно зрозуміти різну «дічь» в Java ;)) IMHO

    навантажений бек, хмарні сервіси, системні речі, cli.... але люди на ньому все пишуть. ну наприклад свіжа доповідь Фіцпатріка (данга, фейсбук тепер тейлскейл).

    А еще у нас, в Киеве, крутейшее go коммьюнити. До пандемии митапы были каждый месяц.

    Це точно, я пам’ятаю і нагадую Артему коли бачу.

    переходил бы уже на что-то нормальное, а не на это...

    Незалежно від відношення до Go, стаття просто крутезна своєю інформативністю.

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

    А какие на нем проекты, что они интересней ?

    У Go більше можливостей для використання, можна орендувати дешеві сервери для мікросервісів (і дорогі для БД як завжди).

    Проектів які можна якісно зробити на Go більше, ніж на PHP, особливо високонавантажених.

    Можеш уявити github.com/cossacklabs/acra на PHP? (з такою ж швидкодією)
    Або розробити свою БД як VictoriaMetrics на PHP?

    в Go видимість визначається через написання першої букви ідентифікатора з великої або малої букви

    Дякую за вакцинацію від go

    Вот кстати да, при всей симпатичности языка так и не понял почему экспорт в Го делается таким... кхм... необычным образом.

    То есть отсутствие точки с запятой тебя НЕ смущает (со всеми вытекающими как в жабаскрипте), всего два модификатора доступа вместо трёх-четырёх, и так далее.

    Да, можно достичь сокращения кода. Но ЦЕНОЙ неоднозначности при прочтении, НЕЯВНЫХ малозаметных ошибок, чрезмерной пунктуации и неадекватным количеством пустого места, которым это всё дерьмо надо будет снабжать.

    Перешли бы уже к иероглифам. Или к переменным в одну букву с цыферками и буковками снизу, сверху, сбоку, и ещё сверху верхних — ну как это сделали математики — да так что математика стала мёртвой наукой, которая мало кем может применяться.

    Я надеюсь, что go не станет новым Жабаскриптом и не наплодит мегатонны сложно поддерживаемого дерьма, которое потом будут без конца переписывать на том же го, только покращенных версий, чтобы по итогу сказать «ваш браузер устарел» (даже при том что система формально не будет использовать браузер).

    IMHO язык должен читаться. Это его основная ценность. Нет читаемости —> нет качества.

    Отсутствие точек с запятой, компилятора и go fmt (форматирования, которым пользуются все) просто убивает проблему с нечитаемостью кода при их отстутствии. точка с запятой уже давно стала обычным рудиментом. Но, конечно, её никто не заставляет не ставить, компилятор это поймет в любом случае (только вот зачем?).

    А модификаторы доступа с помощью заглавной буквы — это вообще гениальное изобретение, которое сокращает написание кода и при этом дает абсолютно всем понять публичный ли это метод\филд или нет.

    Как можно говорить, что Go нечитаемый, когда в той же Java, например, просто кучи бойлерплейта, километровых вызовов методов через тучи точек у объектов?

    Из всех языков, на которых мне доводилось писать Go — чуть ли не самый легкочитаемых из всех.

    Отсутствие точек с запятой, наличие компилятора и go fmt

    не могу изменить сообщение, так что поправлю тут :)

    точку с запятой никто не заставляет не ставить, компилятор это поймет в любом случае (только вот зачем?).

    Именно затем, чтобы НЕ понял. Но если рудимент есть, это как минимум решает проблему нескольких выражений в одной строчке.

    А вот с одним в нескольких — пичалька, и хотя я подозреваю что есть символ переноса, он потом может вылезти боком при дальнейшем рефакоринге или сжатии кода. Как минимум, лишает код читаемости (теоретически, я в го не разбирался, как там это реализовали).

    Основа основ легкочитаемости — скорость разбиения на части, как на крупные, так и на мелкие, притом деление на крупные [блоки] в приоритете. А с легкочитаемостью я уже показал проблему века: имя переменной решает. И если в плюсах это были подчёркивания (весьма заметные и понятные), то с заглавной буквой всё вообще печально для тех, кто переходит с других языков (продолжая их помнить и на них писать), там-то соглашения совсем иные.

    Будь-ласка, раніше також до Go скептично відносився, але зараз Go вже подобається

    Go как наркотик. Распробовав его понимаешь, что слезть с него будет очень и очень трудно :D

    На садомазо теж підсідають, але я б не рекомендував.

    підкажіть навчальні матеріалі для web проектів з react

    Да, поддерживаю, крутая статья)

    Дякую, придивлявся до Go раніше?

    респект за такую статью! круто!
    реквестирую англ версию на медиуме, мало подобных, годных, материалов и этот должен зайти

    Що буде цікавіше почитати нову статтю про Go українською мовою, про тестування, JSON, Protobuf.

    Чи перекласти цю статтю на english яка загубиться в шумі.

    Проектів на Go стає більше на аутсорсі, кандидатів мало, статті українською будуть сприяти збільшенню кандидатів і в майбутньому збільшенню проектів.

    Ось ще історія про технічну статтю англійською:
    www.facebook.com/...​iy/posts/3148985225170792

    Очень сильный материал — большое спасибо !

    Да уж, труд тут титанический, меня бы на это не хватило.

    Столько кода в одной статье на доу не было с самого... да никогда столько когда на доу не было! Круто!

    Подписаться на комментарии