114720

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]
}
Учиться новому проще на фундаменте имеющихся знаний. Сравнивая особенности Python и языка Go, мы находим множество сходств между ними. Это позволяет глубже понять концепции программирования и проще изучить новое.

Оригинальная статья

Книги по Go и другие полезные материалы:

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

admin
11 декабря 2018

ООП на Python: концепции, принципы и примеры реализации

Программирование на Python допускает различные методологии, но в его основе...
admin
28 июня 2018

3 самых важных сферы применения Python: возможности языка

Существует множество областей применения Python, но в некоторых он особенно...
admin
13 февраля 2017

Программирование на Python: от новичка до профессионала

Пошаговая инструкция для всех, кто хочет изучить программирование на Python...