Go vs Python: изучение основ языка Go в сравнении с Python
Это не соревнование двух языков, а просто еще один способ обучения. Рассматриваем возможности языка Go, проводя параллели с Python.
Знаете Python? Бесплатные книги и другие туториалы по столь популярному языку программирования доступны повсеместно. Давайте же разберем на примере Python язык Go.
Go гораздо многословнее, чем Python. Чтобы сказать то же самое, ему требуется больше строк кода. Он меньше, чем Python, подходит для скриптового программирования, однако его популярность в этом качестве растет.
Hello World
Начнем с классики – незаменимой для обучения программы Hello World. В Python мы выводим легендарную фразу всего одной строчкой:
print("Hello world")
В Go придется приложить чуть больше усилий: указать имя главного пакета, подключить модуль fmt
и оформить функцию main
:
package main import "fmt" func main() { fmt.Println("Hello world") }
Вывод данных
Продолжим с выводом и решим три фундаментальные задачи:
- вывод строки с переходом на следующую строку;
- вывод строки без перехода на следующую строку;
- форматированный вывод.
В Python все три вещи может сделать единственная функция print
:
print("Some string") print("Some string", end="") print("Name: {}, Age: {}".format("Peter", 35))
У языка Go есть три разных метода:
package main import "fmt" func main() { fmt.Println("Some string") fmt.Print("Some string") fmt.Printf("Name: %s, Age: %d\n", "Peter", 35) }
Комментарии
Комментарии – важная часть документации кода, и программирование на Go – не исключение.
В языке Python они выглядят так:
"""doc string для целого модуля""" # строчный комментарий class Class(object): """doc string для класса""" print(__doc__) print(Class.__doc__)
Язык программирования Go предлагает широко распространенный синтаксис комментирования:
package main // обычный строчный комментарий /* тоже комментарий только на несколько строк */ /* Многострочный комментарий для функции main(). Чтобы увидеть его, запустите в консоли команду: godoc comments.go */ func main() { }
Многострочные блоки
В языке программирования Python за многострочность отвечают тройные кавычки. Также для оформления строк можно использовать двойные и одинарные кавычки.
print( """Это многострочная строка. """ ) print("O'word " 'Another "word" ' "Last word.")
В Golang для строк используются двойные кавычки, следовательно, сам символ двойной кавычки внутри строки нужно экранировать. Многострочность обеспечивают обратные кавычки.
package main import "fmt" func main() { fmt.Println(`Это многострочная строка. `) fmt.Println( "O'word " + "Another \"word\" " + "Last word.") }
Списки
Срез (slice
) – это последовательность элементов, длина которой может изменяться. Основное различие между массивом и срезом состоит в том, что, работая с массивом, вы должны знать его размер.
Списки в Python:
# инициализируем список numbers = [0] * 5 # изменяем один элемент numbers[2] = 100 some_numbers = numbers[1:3] print(some_numbers) # [0, 100] # получаем размер print(len(numbers)) # 5 # инициализируем другой список scores = [] scores.append(1.1) scores[0] = 2.2 print(scores) # [2.2]
У языка Go нет способа так же легко добавлять значения в существующий срез, поэтому вы можете инициализировать срез с максимальной длиной и постепенно добавлять в него элементы.
package main import "fmt" func main() { // инициализируем массив var numbers [5]int // [0, 0, 0, 0, 0] // меняем один элемент numbers[2] = 100 // создаем срез из массива some_numbers := numbers[1:3] fmt.Println(some_numbers) // [0, 100] // получаем размер fmt.Println(len(numbers)) // инициализируем срез var scores []float64 scores = append(scores, 1.1) // пересоздаем, чтобы добавить элемент scores[0] = 2.2 // изменяем fmt.Println(scores) // [2.2] // когда вы точно не знаете, сколько элементов // собираетесь положить в срез, можно сделать так var things [100]string things[0] = "Peter" things[1] = "Anders" fmt.Println(len(things)) // 100 }
Ассоциативные массивы
В ассоциативных массивах ключ может иметь нечисловое значение.
В Python эту функцию выполняют словари (dictionaries):
elements = {} elements["H"] = 1 print(elements["H"]) # 1 # удалить по ключу elements["O"] = 8 elements.pop("O") # проверка наличия ключа if "O" in elements: print(elements["O"]) if "H" in elements: print(elements["H"])
В Go для этой же цели мы можем использовать отображения:
package main import "fmt" func main() { elements := make(map[string]int) elements["H"] = 1 fmt.Println(elements["H"]) // удаление по ключу elements["O"] = 8 delete(elements, "O") // сделать что-то с элементом, если он есть в отображении if number, ok := elements["O"]; ok { fmt.Println(number) // не будет выведено } if number, ok := elements["H"]; ok { fmt.Println(number) // 1 } }
В Golang даже можно создать отображение отображений:
elements : make(map[string]map[string]int) elements["H"] = map[string]int{ "protons": 1, "neutrons": 0, }
Но помните, что для этой цели существует struct
.
Логические значения
В Python внутри условия if
можно использовать выражения с разными типами данных – в большинстве случаев они автоматически конвертируются в логическое значение True
или False
:
x = 1 if x: print "Yes" y = [] if y: print "не будет выведено" print(True and False) # False print(True or False) # True print(not True) # False
У языка Go нет быстрого способа оценить истинность выражения, поэтому приходится делать это явно для каждого типа данных:
package main import "fmt" func main() { x := 1 if x != 0 { fmt.Println("Yes") } var y []string if len(y) != 0 { fmt.Println("не будет выведено") } fmt.Println(true && false) // false fmt.Println(true || false) // true fmt.Println(!true) // false }
В этих примерах также видна работа основных логических операторов.
Циклы
В Python существует несколько видов циклов:
i = 1 while i <= 10: print(i) i += 1 # ...или... for i in range(1, 11): print(i)
В языке программирования Go – всего один, и это цикл for
:
package main import "fmt" func main() { i := 1 for i <= 10 { fmt.Println(i) i += 1 } // то же самое, но немного удобнее for i := 1; i <= 10; i++ { fmt.Println(i) } }
Перебор элементов
Цикл for
можно использовать для перебора элементов коллекции и в Python:
names = ["Peter", "Anders", "Bengt"] for i, name in enumerate(names): print("{}. {}".format(i + 1, name))
и в Go:
package main import "fmt" func main() { names := []string{ "Peter", "Anders", "Bengt", } /* будет выведено: 1. Peter 2. Anders 3. Bengt */ for i, name := range names { fmt.Printf("%d. %s\n", i+1, name) } }
Switch
Множественные условия могут быть определены с помощью конструкции switch
.
В Python она выглядит так:
def input_(): return int(input()) number = input_() if number == 8: print("Oxygen") elif number == 1: print("Hydrogen") elif number == 2: print("Helium") elif number == 11: print("Sodium") else: print("I have no idea what %d is" % number) # Альтернативное решение number = input_() db = {1: "Hydrogen", 2: "Helium", 8: "Oxygen", 11: "Sodium"} print(db.get(number, "I have no idea what %d is" % number))
А вот вариант кода для Go:
package main import ( "fmt" "strconv" ) func str2int(s string) int { i, err := strconv.Atoi(s) if err != nil { panic("Not a number") } return i } func main() { var number_string string fmt.Scanln(&number_string) number := str2int(number_string) switch number { case 8: fmt.Println("Oxygen") case 1: fmt.Println("Hydrogen") case 2: fmt.Println("Helium") case 11: fmt.Println("Sodium") default: fmt.Printf("I have no idea what %d is\n", number) } // Альтернативное решение fmt.Scanln(&number_string) db := map[int]string{ 1: "Hydrogen", 2: "Helium", 8: "Oxygen", 11: "Sodium", } number = str2int(number_string) if name, exists := db[number]; exists { fmt.Println(name) } else { fmt.Printf("I have no idea what %d is\n", number) } }
Как видно из примеров, оба языка предлагают более удобные альтернативные решения задачи с использованием ассоциативных массивов.
Вариативные функции
В Python функции могут принимать переменное число аргументов различных типов с помощью somefunction (*args)
.
def average(*numbers): return sum(numbers) / len(numbers) print(average(1, 2, 3, 4)) # 10/4 = 2.5
В Go так нельзя. Однако есть возможность указать параметр, представляющий собой неопределенное количество значений одного конкретного типа:
package main import "fmt" func average(numbers ...float64) float64 { total := 0.0 for _, number := range numbers { total += number } return total / float64(len(numbers)) } func main() { fmt.Println(average(1, 2, 3, 4)) // 2.5 }
Измерение времени
В Python методы работы со временем собраны в модуле time
:
import time t0 = time.time() time.sleep(3.5) t1 = time.time() print("Took {:.2f} seconds".format(t1 - t0))
В языке программирования Go, как ни странно, тоже есть такой модуль:
package main import "fmt" import "time" func main() { t0 := time.Now() elapsed := time.Since(t0) fmt.Printf("Took %s", elapsed) }
Замыкания функций
Внутри одной функции можно спрятать другую, так что она становится недоступной для внешнего кода.
Вот так можно реализовать замыкание в Python:
def run(): def increment(amount): return number + amount number = 0 number = increment(1) number = increment(2) print(number) # 3 run()
Обратите внимание, что используемая во внутренней функции переменная number доступна из скоупа внешней функции. Если бы ее там не было, возникла бы ошибка UnboundLocalError.
def increment(amount): number += amount increment(1) increment(2)
Нужную переменную можно объявить прямо внутри increment
. А чтобы она была доступна на внешнем уровне, используется ключевое слово global
.
def increment(amount): global number number += amount increment(1) increment(2)
А вот пример замыкания в Golang:
package main import "fmt" func main() { number := 0 /* Это должна быть именно локальная переменная. Нельзя написать `func increment(amount int) {` */ increment := func(amount int) { number += amount } increment(1) increment(2) fmt.Println(number) // 3 }
Defer
В Go defer
имеет замечательный синтаксис. Отложенная операция стоит сразу после ключевого слова, так что смысл конструкции сразу же становится очевиден:
package main import ( "os" ) func main() { f, _ := os.Open("defer.py") defer f.Close() // теперь вы можете читать из f // он будет закрыт в конце программы }
В Python вы можете добиться того же, поместив код в try-finally
блок:
f = open("defer.py") try: f.read() finally: f.close()
Panic Recover
Чтобы сгенерировать и поймать ошибку, в Python используется ключевое слово raise
и конструкция try-except
:
try: raise Exception("Shit") except Exception as e: print("error was:", e)
В Go для этого есть оператор panic
и функция recover
:
package main import "fmt" func main() { // будет выведено: // error was: Shit! defer func() { fmt.Println("error was:", recover()) }() panic("Shit!") }
Изменяемость
Передавая параметром в функцию сложные структуры, например, массив или мапу, вы можете изменить прямо внутри функции, и это автоматически изменится снаружи.
В Python:
def upone(mutable, index): mutable[index] = mutable[index].upper() list_ = ["a", "b", "c"] upone(list_, 1) print(list_) # ['a', 'B', 'c'] dict_ = {"a": "anders", "b": "bengt"} upone(dict_, "b") print(dict_) # {'a': 'anders', 'b': 'BENGT'}
В Golang:
package main import ( "fmt" "strings" ) func upone_list(thing []string, index int) { thing[index] = strings.ToUpper(thing[index]) } func upone_map(thing map[string]string, index string) { thing[index] = strings.ToUpper(thing[index]) } func main() { // mutable list := []string{"a", "b", "c"} upone_list(list, 1) fmt.Println(list) // [a B c] // mutable dict := map[string]string{ "a": "anders", "b": "bengt", } upone_map(dict, "b") fmt.Println(dict) // map[a:anders b:BENGT] }
Структуры
В Python для создания пользовательских наборов полей используются классы. Метод __init__
вызывается при создании экземпляра класса:
from math import sqrt class Point(object): def __init__(self, x, y): self.x = x self.y = y def distance(point1, point2): return sqrt(point1.x * point2.x + point1.y * point2.y) p1 = Point(1, 3) p2 = Point(2, 4) print(distance(p1, p2)) # 3.74165738677
В Go существует специальное ключевое слово struct
:
package main import ( "fmt" "math" ) type Point struct { x float64 y float64 } func distance(point1 Point, point2 Point) float64 { return math.Sqrt(point1.x*point2.x + point1.y*point2.y) } // Так как структуры автоматически копируются при передаче, // лучше передавать их в функцию как указатели func distance_better(point1 *Point, point2 *Point) float64 { return math.Sqrt(point1.x*point2.x + point1.y*point2.y) } func main() { p1 := Point{1, 3} p2 := Point{2, 4} fmt.Println(distance(p1, p2)) // 3.7416573867739413 fmt.Println(distance_better(&p1, &p2)) // 3.7416573867739413 }
Методы
В Python методы определяются прямо внутри классов с помощью ключевого слова def
, и первым аргументом принимают ссылку на экземпляр класса:
from math import sqrt class Point(object): def __init__(self, x, y): self.x = x self.y = y def distance(self, other): return sqrt(self.x * other.x + self.y * other.y) p1 = Point(1, 3) p2 = Point(2, 4) print(p1.distance(p2)) # 3.74165738677 print(p2.distance(p1)) # 3.74165738677
Методы в Go – это функции, прикрепленные к какому-то конкретному типу. Его параметр обязательно указывается в сигнатуре метода:
package main import ( "fmt" "math" ) type Point struct { x float64 y float64 } func (this Point) distance(other Point) float64 { return math.Sqrt(this.x*other.x + this.y*other.y) } func (this *Point) distance_better(other *Point) float64 { return math.Sqrt(this.x*other.x + this.y*other.y) } func main() { p1 := Point{1, 3} p2 := Point{2, 4} fmt.Println(p1.distance(p2)) // 3.7416573867739413 fmt.Println(p1.distance_better(&p2)) // 3.7416573867739413 }
Горутины
Горутины языка Go – это независимые параллельные операции. Сама функция main
является горутиной. Для их определения используется оператор go
:
package main import ( "fmt" "io/ioutil" "net/http" "sync" ) func f(url string) { response, err := http.Get(url) if err != nil { panic(err) } defer response.Body.Close() body, err := ioutil.ReadAll(response.Body) if err != nil { panic(err) } fmt.Println(len(body)) } // Смотрите пример на https://golang.org/pkg/sync/#WaitGroup func main() { var wg sync.WaitGroup urls := []string{ "https://www.peterbe.com", "https://python.org", "https://golang.org", } for _, url := range urls { wg.Add(1) go func(url string) { defer wg.Done() f(url) }(url) } // ждем, когда горутина закончит работу wg.Wait() }
В Python параллельное программирование также возможно с помощью модуля multiprocessing
:
import urllib2 import multiprocessing def f(url): req = urllib2.urlopen(url) try: print(len(req.read())) finally: req.close() urls = ("https://www.peterbe.com", "https://python.org", "https://golang.org") if __name__ == "__main__": p = multiprocessing.Pool(3) p.map(f, urls)
Args
Часто бывает необходимо получить аргументы переданные в команде терминала. Рассмотрим эту задачу на примере команды:
go run args.go peter anders bengt
Программа должна вернуть все три аргумента в верхнем регистре:
PETER ANDERS BENGT
В Python для разбора неопределенного количества неименованных параметров удобно воспользоваться синтаксисом *args
. Список аргументов находится в sys.argv
.
import sys def transform(*args): for arg in args: print(arg.upper()) if __name__ == "__main__": transform(*sys.argv[1:])
В Go аргументы командной строки хранятся в os.Args
:
package main import ( "fmt" "os" "strings" ) func transform(args []string) { for _, arg := range args { fmt.Println(strings.ToUpper(arg)) } } func main() { args := os.Args[1:] transform(args) }
Псевдонимы для импорта
В Python для импортируемых модулей можно использовать алиасы – короткие псевдонимы:
import string as s print(s.upper("world"))
У языка Go тоже есть такая возможность:
package main import ( "fmt" s "strings" ) func main() { fmt.Println(s.ToUpper("world")) }
Конечно, вы обычно не используете псевдонимы для встроенных модулей с короткими именами, но вот здесь, например, это может быть удобно:
import ( pb "github.com/golang/groupcache/groupcachepb" )
Также можно импортировать пакеты, которые фактически не будут использоваться:
import ( _ "image/png" )
Быстрое форматирование строк
Строки не всегда нужно печатать, иногда требуется просто быстро их отформатировать и передать, куда следует. В Python для этого есть простая функция f
:
max = 10 raise Exception(f"The max. number is {max}")
В Golang для быстрого форматирования используется метод Sprintf
модуля fmt
:
package main import "fmt" func main() { max := 10 panic(fmt.Sprintf("The max. number is %d", max)) }
Отбор уникальных значений
Задача убрать из коллекции все повторяющиеся элементы встречается очень часто. Python предлагает аккуратное решение, которое полностью зависит от типа, если значения поддерживают хэширование.
def uniqify(seq): seen = {} unique = [] for item in seq: if item not in seen: seen[item] = 1 unique.append(item) return unique items = ["B", "B", "E", "Q", "Q", "Q"] print(uniqify(items)) # prints ['B', 'E', 'Q']
А здесь вы можете найти самый быстрый способ унифицировать список в Python.
Вот аналогичный код для языка Go:
package main import "fmt" func uniqify(items []string) []string { uniq := make([]string, 0) seen := make(map[string]bool) // для большей эффективности: // seen := make(map[string]struct{}) // https://stackoverflow.com/questions/37320287/maptstruct-and-maptbool-in-golang for _, i := range items { if _, exists := seen[i]; !exists { uniq = append(uniq, i) seen[i] = true } } return uniq } func main() { items := []string{"B", "B", "E", "Q", "Q", "Q"} items = uniqify(items) fmt.Println(items) // prints [B E Q] }