Всем привет! На связи Пётр, Go-разработчик в команде Ozon, которая занимается управлением товарами торговой площадки. Всё, что загружают продавцы, обрабатывается нашими сервисами. Девять месяцев назад я сменил основной язык программирования с C# на новый для меня Go. В статье будут впечатления от Go, расскажу о некоторых различиях между языками, а в конце поделюсь своим опытом поиска работы на новом языке. Ведь вопрос смены стека технологий рано или поздно встаёт перед каждым разработчиком.

Почему Go?

На шарпе я проработал два с половиной года. Сначала разрабатывал программное обеспечение для предприятий нефтегазовой отрасли. Мы делали монолитные десктопные приложения на .NET Framework, WPF и WinForms. Через год с небольшим захотелось перемен, и я перешёл в компанию, осуществляющую денежные переводы по Европе. На собеседовании мне обещали распиливание монолитного ядра (снова .NET Framework). Но за год позволили вынести лишь небольшую часть кода в отдельный сервис на .NET 5.0. В основном я писал бизнес-логику на хранимых процедурах.

Нельзя сказать, что меня устраивала эта работа. Я искал что-то новое, читал новости в Telegram-каналах и стал везде встречать информацию о Go. Узнал, что Docker, Kubernetes, Jaeger и другие известные продукты написаны на нём. Многие компании, такие как Netflix, Twitch, Dropbox, переносят на него требовательные к производительности части ПО. В России его используют Яндекс, Авито, Ozon, VK и другие IT-гиганты. Когда я искал первую работу на Go, столкнулся с двумя компаниями, которые переходили на этот язык с C#: первая занималась разработкой игровой платформы,  вторая — развитием облачного хранилища.

Go — хорошо оплачиваемый язык

В отчёте Хабр Карьеры за второе полугодие 2021 года Go разделил со Swift третье место в России, а через шесть месяцев вышел на чистое третье место:

Ёмкость рынка

Осенью 2021 года вакансий на C# было в два раза больше, чем на Go (я сравнивал поисковую выдачу HeadHunter в Москве по ключевым словам «C# разработчик» и «Go разработчик»). Но меня это не смутило, так как рынок в то время был очень горячим. Чувствовалось, что, имея два с половиной года коммерческого опыта, проблем с переходом на новый язык не будет.

Доступность обучения

Мне часто попадалась реклама курсов по Go от Skillbox, GeekBrains, Слёрма и других проектов. Яндекс Практикум заканчивал подготовку курса «Продвинутый Go-разработчик». Осенью в Ozon стартовал первый набор на бесплатные курсы Route 256. Для получения недостающих знаний о микросервисах на C# я записался на соответствующий курс. Тогда же увидел рекламу бесплатной программы «Golang разработчик» от CloudMTS. Этапы отбора не были сложными, поэтому я попал и туда. Так началось знакомство с новым языком.

Синтаксис

Первое, что бросилось в глаза, — простота синтаксиса. Она действительно обеспечивает низкий порог входа. Go немногословен. Согласно документации, в языке всего 25 ключевых слов:

break

default

func

goto

select

case

defer

if

map

struct

chan

else

import

package

switch

const

fallthrough

interface

range

type

continue

for

go

return

var

В C# вы найдёте аж 77 ключевых слов. Разница более чем в три раза! Шарп многословен, особенно это касается модификаторов доступа: public, private, internal и прочих. Часто требуется добавлять к ним ключевое слово static для указания того, что методы и классы будут использоваться без создания экземпляра класса. Если же в коде есть наследование, то описание метода усложняется ключевыми словами virtual, new, abstract, override.

В Go при объявлении структуры можно управлять видимостью поля с помощью регистра первой буквы. Также можно встраивать структуры друг в друга и перечислять поля одного типа через запятую (подобная запись в C# нарушит соглашение о написании кода):

type baseItem struct {
    baseID int64
}

type Item struct {
    baseItem
    id, otherID int64
    Description string
}

Определение типа происходит очень просто. Но для доступа к внутренним полям структуры придётся писать геттеры. В C# с этой задачей прекрасно справляются свойства.

Синтаксис Go полностью соответствует замыслу: любые операции должны быть описаны явно. Следование ему помогает добиться высокой производительности. Однако приходится расплачиваться поддерживаемостью и читаемостью кода. Практически полное отсутствие синтаксического сахара усложняет написание простых операций на Go. 

Возьмём для примера следующую задачу. Есть списки людей и компаний. Необходимо распечатать информацию о сотруднике и компании, в которой он работает:

type Person struct {
   Name      string
   CompanyID int
}

type Company struct {
   ID              int
   Title, Language string
}

type Employee struct {
   Name, Company, Language string
}

func main() {
   var persons = []Person{
       {"Petya", 1},
       {"Tanya", 2},
       {"Misha", 4},
       {"Sasha", 2},
   }

   var companies = []Company{
       {1, "Ozon", "Go"},
       {2, "Yandex", "C#"},
       {3, "Sber", "Java"},
       {4, "VK", "Python"},
   }

   companySet := make(map[int]Company)
   for _, company := range companies {
   	companySet[company.ID] = company
   }
	
   var employees []Employee
   for _, p := range persons {
      if company, ok := companySet[p.CompanyID];ok  {
         emp := Employee{p.Name, company.Title, company.Language}
         employees = append(employees, emp)			
      }
   }

   for _, emp := range employees {
       fmt.Printf("%s - %s (%s)\n", emp.Name, emp.Company, emp.Language)
   }
}

Если для решения этой задачи воспользоваться C#, то сразу несколько возможностей языка сделают код более понятным:

  • записи для краткого объявления DTO (data transfer object) — объектов для передачи данных;

  • анонимные типы от избыточных объявлений DTO;

  • LINQ to Objects для удобной работы с коллекциями;

  • упрощённое форматирование строк с помощью интерполяции. 

В итоге получится лаконичный код, который к тому же меньше по размеру:

record Person(string Name, int CompanyId);

record Company(int Id, string Title, string Language);
             
public static void Main()
{   
    var people = new Person[]
    {
        new ("Petya", 1),
        new ("Tanya", 2),
        new ("Misha", 4),
        new ("Sasha", 1),
    };

    var companies = new Company[]
    {
        new (1, "Ozon", "Go"),
        new (2, "Yandex", "C#"),
        new (3, "Sber", "Java"),
        new (4, "VK", "Python")
    };

    var employees = people.Join(companies,
        p => p.CompanyId,
        c => c.Id,
        (p, c) => new { p.Name, c.Title, c.Language });
    
    foreach (var emp in employees)
        Console.WriteLine($"{emp.Name} - {emp.Title} ({emp.Language})");
}

Структура проектов

С# и Go сильно отличаются организацией кода в проекте. При использовании языков с полновесным ООП код разбивается на классы. Как правило, один класс — один файл. Далее логически объединённые файлы раскладываются по папкам и подпапкам. Например, так будет выглядеть пространство имён Drivers, которому соответствует одноимённая папка с драйверами для подключения к различным СУБД:

Drivers/
    Sqlite.cs
    Postgres.cs
    Mysql.cs
    Clickhouse.cs

Имея полное имя класса, компилятор знает, какой именно код нужен для сборки проекта. Если мы пишем var sqlite = new Drivers.Sqlite(), то компилятор будет использовать только зависимость из Sqlite.cs.

Что же произойдёт, если в Go-проекте будет похожая папка?

driver/
    sqlite.go
    postgres.go
    mysql.go
    clickhouse.go

Единица компиляции в Go — пакет. Он представляет собой файлы с расширением .go в одноимённой директории. В нашем примере внутри пакета drivers имеется код для подключения ко всем СУБД. При компиляции будут подтягиваться ненужные зависимости. А при исправлении ошибки в коде для ClickHouse появится обновление для всех клиентов, использующих данный пакет. Поэтому файлы следует разнести по отдельным папкам. Чтобы не получались пакеты с большим количеством кода (тысячи строк), следует логически связанные блоки выносить в соседние файлы. Если обратить внимание на многие реальные Go-проекты, то в них неожиданно мало пакетов, однако в пакетах много файлов с сотнями и даже тысячами строк. А в проектах, написанных на языках с полноценным ООП, будет очень много вложенных файлов с парой десятков строк.

Подводные камни срезов

В C# параметры в методы передаются по значению или по ссылке. В Go всё передаётся по значению, то есть копируется. Но в нём есть указатели, поэтому копирование указателя равносильно передаче параметров по ссылке. Конечно, нужно вручную указать, хочешь ли ты передать в метод данные или только ссылку на них, однако в целом поведение обоих языков схожее. 

Но есть нюансы! Возьмём для примера C#-код с добавлением элементов в динамический массив List:

public static void Main()
{
    var list = new List<int>{1, 2, 3};
    Print(list);    // 1 2 3
    Add(list, new List<int>{4, 5});
    Print(list);    // 1 2 3 4 5
}

public static void Add(List<int> list, List<int> values)
{
    list.AddRange(values);
}

Код ведёт себя ожидаемо: в переменной list было три элемента, передали её по ссылке в метод, добавили два элемента. Так как классы передаются по ссылке, то после метода получаем в коллекции пять элементов.

Аналог динамического массива в Go — срез (или слайс). Он представляет собой структуру, состоящую из ссылки на массив, количества выделенных для заполнения ячеек памяти и количества заполненных ячеек памяти. Убедимся, что внутри среза находится указатель на массив с данными:

func main() {
    slice := []int{1, 2, 3}
    fmt.Println("[1]", slice)    // 1 2 3
    changeFirst(slice, -1)
    fmt.Println("[2]", slice)    // -1 2 3
}

func changeFirst(sl []int, first int) {
    sl[0] = first
}

Изменился первый элемент среза. Следовательно, при работе с ним мы имеем ссылку на массив. Благодаря этому срезы очень широко применяются в Go. В отличие от массивов, которые копируются полностью при передаче в функции.

Однако всё усложняется, когда требуется изменить количество элементов в срезе. Вернёмся к примеру на C#, в котором мы добавляли элементы в конец коллекции, и перепишем его на Go:

func main() {
    slice := []int{1, 2, 3}
    fmt.Println("[1]", slice)    // 1 2 3
    add(slice, []int{4, 5})
    fmt.Println("[2]", slice)    // 1 2 3
}

func add(sl []int, values []int) {
    sl = append(sl, values...)
    fmt.Println("add", sl)    // 1 2 3 4 5
}

Как видим, в функции количество цифр в срезе изменилось, однако в main() оно осталось прежним. Почему же так произошло? В функции append() выделяется память под новый увеличенный массив. Получается, что в main() внутри среза указатель на один массив, а внутри add() мы распечатываем срез со ссылкой на другой массив. 

Это лишь один из многих примеров неочевидного поведения в Go. На Хабре ему посвящена пара отличных статей: «Как не наступать на грабли в Go» и «50 оттенков Go: ловушки, подводные камни и распространённые ошибки новичков». Необходимо время, чтобы разобраться с ним и не делать простых ошибок. 

В свою очередь, C# более предсказуем. При изучении языка и в начале карьерного пути хлопоты мне доставляло поведение ссылочных типов, используемых в разных частях проекта. Но это было не проблемой языка, а следствием отсутствия опыта или плохой архитектуры. 

Горутины и каналы

До непосредственного знакомства с Go я часто слышал о загадочных горутинах и каналах. Язык отлично подходит для многопоточной разработки. Чтобы создать «легковесный поток» — горутину — достаточно лишь перед вызовом функции написать “go”. Для примера запустим десять горутин в цикле и выведем в консоль номера итераций:

func main() {
    for i := 0; i < 10; i++ {
    go func(x int) {
        fmt.Println(x)
    }(i)
}

И в консоли не будет ничего напечатано! Потому что Go не использует концепцию ожидания в отличие от C#. Разработчик, владеющий шарпом, ожидает поведения, вызванного таким кодом:

public static void Main()
{     
    Parallel.For(1, 10, i => Console.WriteLine(i));
}

Или каким-то подобным:

public static async Task Main()
{                
    var tasks = new List<Task>(10); 
    for (int i = 0; i < 10; i++)
    {
        var x = i;
        tasks.Add(Task.Run(() => Console.WriteLine(x)));               
    }
    await Task.WhenAll(tasks);           
}

Но на самом деле получается код, аналогичный этому:

public static void Main()
{             
    for (int i = 0; i < 10; i++)
    {
        var x = i;
        Task.Run(() => Console.WriteLine(x));               
    }                 
}

Запускается десять задач на выполнение — и не ожидается окончание этого процесса. Поэтому в Go необходимо синхронизировать выполнение горутин. 

В приведённом примере можно воспользоваться структурой WaitGroup из пакета sync:

func main() {
    wg := &sync.WaitGroup{}
    wg.Add(10)
    for i := 0; i < 10; i++ {
        go func(x int) {
            defer wg.Done()
            fmt.Println(x)
           
        }(i)
    }
    wg.Wait()
}

Перед циклом происходит объявление, что будут выполняться десять горутин. Внутри структуры WaitGroup значение счётчика выставляется равным 10. В каждой горутине уменьшаем его на 1 с помощью метода Done(). А в конце функции main() ожидаем, когда значение счётчика будет равно 0. И только потом завершает своё выполнение главная горутина.

Пара слов о каналах

Передача данных между потоками может осуществляться либо с помощью разделяемой памяти (как в предыдущем примере), либо через обмен сообщениями. И реализация второго принципа делает Go действительно мощным инструментом для высоконагруженных многопоточных приложений. В документации сказано: 

«Don’t communicate by sharing memory, share memory by communicating». 

Общей памятью при «общении» горутин являются отдельные структуры — каналы. Канал представляет собой трубу (pipe), в которую один или несколько потоков кладут данные, а один или несколько других потоков их из неё забирают. Благодаря этому возможно просто и эффективно реализовывать такие шаблоны конкурентности, как генератор (реализация ниже), распылитель, очередь, конвейер, набор обработчиков и прочие. 

Параллельное программирование

Go — молодой язык. Его релиз состоялся в 2009 году, а создание было реакцией компании Google на активное развитие многоядерности процессоров. Поэтому горутины изначально не завязаны на потоки операционной системы, а рантайм предназначен для того, чтобы добиваться максимальной производительности от всех ядер. И специально для этого были созданы каналы и горутины. Многопоточность — конёк Go. 

Шарп появился в 2001 году. Тогда же IBM представила свой первый двухъядерный процессор для широкого использования — POWER4. Так что язык создавался для других задач и на другом фундаменте. Однако в 2010 году вышла версия 4.0 с библиотекой параллельных задач TPL (Task Parallel Library), концепцией задач и классами Task, TaskFactory и Parallel. В последующих релизах Microsoft усиливала в языке асинхронность и параллельность. На C# можно решать любые задачи, причём зачастую несколькими способами. Однако многопоточная обработка с обменом сообщениями между потоками на Go пишется быстрее и получается лаконичнее. 

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

Пример на Go:

func main() {
   rand.Seed(time.Now().UnixNano())
   ch := generate(5)
   for item := range ch {
       fmt.Println(item)
   }
}

func generate(count int) chan string {
   ch := make(chan string)
   go func() {
       for i := 0; i < count; i++ {
           time.Sleep(time.Second)
           ch <- fmt.Sprint(rand.Intn(10))
       }
       close(ch)
   }()
   return ch
}

А вот так это будет выглядеть на C#:

public static async Task Main()
{   
    var channelReader = Generate(5);
    await foreach (var item in channelReader.ReadAllAsync())
        Console.WriteLine(item);
}

public static ChannelReader<string> Generate(int count)
{
    var ch = Channel.CreateUnbounded<string>();
    var rnd = new Random();

    Task.Run(async () =>
    {
        for (var i = 0; i < count; i++)
        {
            await Task.Delay(TimeSpan.FromSeconds(1));
            var delay = rnd.Next(10).ToString();
            await ch.Writer.WriteAsync(delay);                   
        }
            ch.Writer.Complete();
    });

    return ch.Reader;
}

В данном случае отсутствие синтаксического сахара не только не мешает Go решать задачу, но и упрощает поддержку за счёт лаконичности кода.

Дженерики

В C# активно используются обобщённые типы. В Go до релиза версии 1.18 (февраль 2021 года) их не было вовсе. Но по проектам, в которых я работал, незаметно, чтобы отсутствие дженериков сильно затрудняло написание кода. Чего не скажешь о пакетах-библиотеках. С обобщёнными типами можно не повторять код в разных функциях. Например, разбиение среза элементов любых типов на пачки:

func Batch[T any](items []T, size int) [][]T {
    if len(items) == 0 {
        return [][]T{}
    }

    batches := make([][]T, 0, (len(items)+size-1)/size)
    for size < len(items) {
        items, batches = items[size:], append(batches, items[0:size:size])
    }
    batches = append(batches, items)

    return batches
}

Раньше можно было написать аналогичную функцию, которая принимает срез пустых интерфейсов в качестве первого параметра:

func Batch(items []interface{}, size int) [][]interface{} {
    ...
}

Однако при вызове функции со срезом строк в качестве первого параметра компилятор выдаст ошибку: «Невозможно использовать срез строк в качестве среза пустых интерфейсов». Чтобы всё-таки воспользоваться функцией, необходимо сначала выполнить указанное приведение вручную:

sl := []string{"1", "2", "3", "4", "5"}
ifaceSl := make([]interface{}, 0, len(sl))
for _, item := range sl {
    ifaceSl = append(ifaceSl, item)
}

Поэтому раньше разработчикам приходилось писать функции для каждого типа: StringBatch, Int64Batch, SomeStructBatch и прочих.

Интерфейсы

В C# активно используются интерфейсы. Для того чтобы класс им соответствовал, необходимо не только реализовать их методы, но и явно перечислить интерфейсы при объявлении класса:

interface IGlorifier
{
    void Glorify();
}

class GoGlorifier : IGlorifier
{
    public void Glorify()
    {
        Console.WriteLine("Go is the best!!!");
    }
}

class SharpGlorifier : IGlorifier
{
    public void Glorify()
    {
        Console.WriteLine("C# is the best!!!");
    }
}
      
public static void Main()
{    
    new List<IGlorifier>{new GoGlorifier(), new SharpGlorifier()}
        .ForEach(g => g.Glorify());
}

В примере объявляется интерфейс. Два класса ему соответствуют. Затем в Main() вызывается реализованный каждым метод. 

Go поддерживает утиную типизацию: 

«Если это выглядит как утка, плавает как утка и крякает как утка, то это, вероятно, утка». 

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

type Glorifier interface {
   Glorify()
}

type GoGlorifier struct{}

func (GoGlorifier) Glorify() {
   fmt.Println("Go is the best!!!")
}

type SharpGlorifier struct{}

func (SharpGlorifier) Glorify() {
   fmt.Println("C# is the best!!!")
}

func main() {
   glorifiers := []Glorifier{GoGlorifier{}, SharpGlorifier{}}
   for _, g := range glorifiers {
       g.Glorify()
   }
}

На собеседованиях любят спрашивать, где следует объявлять интерфейс: в пакете, в котором он реализуется, или в том, в котором используется? Правильный ответ — второй. Нашему коду требуются от типа определённые методы, и мы явно это указываем. Если объявить интерфейс в другом пакете, то получится ненужная зависимость, при обновлении которой может сломаться код.

Поиск работы 

В начале декабря 2021 года стал подходить к концу курс по Go. Тогда же назрело желание сменить работу. Сначала решил попробовать сменить язык программирования, а в случае неудачи — продолжить развиваться в C#. Открыл резюме на HeadHunter и Хабр Карьере, а уже через пару дней закрыл: ближайшие две недели были расписаны собеседованиями. Работодателей не смущало отсутствие коммерческого опыта на Go. Приятно поразило большое количество предложений на высоконагруженные проекты. Это именно то, чем хотелось заниматься. На первых технических встречах я проваливался на вопросах о выделении памяти при добавлении элементов в срезы и о работе указателей. Но пробелы в знаниях быстро заполнялись — и через две недели на руках было два оффера.

Ранее я упоминал, что на шарпе можно решить любую задачу несколькими способами. Следовательно, нужно иметь более объёмные познания, чтобы принимать оптимальные решения. Так как Go имеет простой синтаксис, то и материала для подготовки к собеседованиям довольно немного. Бросилось в глаза, что на каждой встрече спрашивают почти одно и то же:

  • область видимости, встраиваемые типы;

  • указатели;

  • интерфейсы, утиная типизация;

  • массивы;

  • внутреннее устройство срезов, выделение памяти при append();

  • внутреннее устройство мапы, эвакуация данных при увеличении мапы;

  • каналы, типы каналов; 

  • горутины, синхронизация горутин;

  • рантайм.

Вопросы же по С# более обширны:

  • ссылочные и значимые типы;

  • стек и куча;

  • парадигмы ООП;

  • интерфейсы и абстрактные классы;

  • весь SOLID;

  • разновидности коллекций;

  • большой пласт про асинхронность, многопоточность и конкурентность;

  • сборщик мусора;

  • контейнер зависимостей;

  • Entity Framework Core;

  • ASP.NET Core.

Неспроста о шарпе написано множество толстых книг. Достаточно вспомнить широко известный труд Рихтера, к которому я подступался несколько раз, однако так и не побывал на всех страницах. Что-то сопоставимое по объёму для изучения Go я не встречал.

Заключение

Я обратил внимание лишь на несколько аспектов, удививших меня при знакомстве с Go. C# и Go очень разные, несмотря на то что оба являются C-подобными, компилируемыми и статически типизированными языками. Однако шарписту среднего уровня довольно несложно перейти на Go. Помогут:

  • простой синтаксис;

  • понимание интерфейсов и внедрения зависимостей;

  • удобная работа с многопоточностью;

  • обилие вакансий с высоконагруженными приложениями;

  • меньше легаси на проектах;

  • ограниченность круга тем на собеседованиях;

  • более высокая оплата труда (правда, зависит от компании).

Освоение языка будут затруднять:

  • отсутствие некоторых привычных функций в стандартных библиотеках;

  • малое количество синтаксического сахара;

  • постоянная необходимость императивно обрабатывать коллекции;

  • немедленная обработка ошибок после каждой функции;

  • иная структура проектов;

  • большее количество подводных камней в языке;

  • непривычный обмен данными между потоками.

Тем не менее Go прекрасно справляется с задачей, для решения которой был создан. Доставляет удовольствие писать на нём надёжные сложные приложения!


Если вам интересно попробовать перейти на Go, то уже в сентябре стартует 4 поток бесплатного курса по Go-разработке для мидлов.

Комментарии (40)


  1. a-tk
    26.08.2022 13:03
    +2

    А где в последнем списке generic-и?


    1. Sitro23 Автор
      26.08.2022 23:55
      -2

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


      1. a-tk
        27.08.2022 09:14
        +4

        А Вы точно C# активно пользовали до Go?


      1. JekaMas
        27.08.2022 17:04
        -2

        Странно, у меня почти везде generics заменили интерфейсы...


        1. aceofspades88
          27.08.2022 19:19
          +1

          интерфейс есть контракт, можно поинтересоваться как у вас дженерики его заменили?


          1. 0xd34df00d
            27.08.2022 23:36
            +2

            В промышленных языках здорового человека есть bounded polymorphism, позволяющий задавать ограничения на типы-параметры. Может, JekaMas пишет не на го.


        1. a-tk
          28.08.2022 21:33

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

          Обобщения позволяют выполнить схожие действия над разнородными данными, не обращая внимания на устройство этих данных. Например, хранить в списке со всеми вытекающими.

          Вот старый-старый тред на этот счёт на RSDN: http://rsdn.org/forum/philosophy/2853873


      1. euroUK
        28.08.2022 00:21

        То, что вам не нужны дженерики лишь говорит о качекстве ваших проектов.

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


  1. mirmikot
    26.08.2022 13:30
    +1

    Хорошая статья, подробная. Да и владеть двумя достаточно современными ЯП - тоже круто.


  1. miga
    26.08.2022 13:41
    +3

    где следует объявлять интерфейс: в пакете, в котором он реализуется, или в том, в котором используется? Правильный ответ — второй. Нашему коду требуются от типа определённые методы, и мы явно это указываем. Если объявить интерфейс в другом пакете, то получится ненужная зависимость, при обновлении которой может сломаться код.

    Это, скажем так, весьма небесспорное утверждение. Есть общепринятая присказка "accept interfaces, return structs", однако она никак не регулирует место, в котором интерфейсы определяются. Кроме того, если вы какие-то методы используете, то зависимость получается как бы всегда, и при обновлении пакетов код может сломаться в любом случае, если сигнатуры поменялись. А от того, что вы формально расширили свои зависимости от минимально необходимых до минимально предоставляемых, компилятор не помрет.

    С другой стороны, когда пакет явно и в одно месте определяет интерфейсы, которые он предоставляет - это гораздо удобнее чем искать все публичные методы типов в этом пакете. Так что мы, например, пакеты пишем как-то так:

    type Smth interface { ... }
    type impl struct { ... }
    
    func NewSmth (...) Smth {
      ...
      return impl{...}
    }
    
    func (i impl) Func1(...) {
      ...
    }
    ...

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


    1. miga
      26.08.2022 14:01

      Более того, выкусывая подобным образом методы из типов, вы ломаете всю иерархию зависимостей (в плане типов языка, не в плане пакетов/модулей) у себя в приложении, что может стрельнуть неожиданным образом, начиная от DI-штук, кончая навигацией по коду ("таак, где у нас этот тип используется? эээ, ой").

      Конечно, не стоит все воспринимать как догму, вполне допустимо использовать подобным образом всякие простецкие интерфейсы из одного-двух очень общих методов, типа Stringer, Reader, ReadWriter итд.


  1. AlexDevFx
    26.08.2022 14:48
    +16

    Пишу на C#. Щупал Go. ИМХО, для Go больше подходит для системного программирования и для каких-нибудь фоновых обработчиков. На данный момент для Web API все же C# предпочтительней, благодаря более развитому ООП. Ну и наличие продвинутых фреймворков и библиотек перевешивает чашу весов на сторону шарпа.
    Под солнцем всем языкам хватает места. Благо есть выбор - под разные нужды свой язык.


    1. calculator212
      26.08.2022 15:44
      -3

      На данный момент для Web API все же C# предпочтительней,

      Web API - это же вы rest имеет ввиду? В целом на го написать бекэнд можно, но наверное самая большая проблема в том, что полноценных фреймворков уровня Spring нет и многие вещи приходится писать самому. Соответственно требования к разработчикам и времени на проект возрастают, что просто невыгодно. Поэтому го часто используют для каких-то отдельных кусков проекта.

      благодаря более развитому ООП

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

      Go больше подходит для системного программирования и для каких-нибудь фоновых обработчиков.

      Он подходит для этого и на го пишут довольно много интересного софта, но 95% реальной работы связано с сетью и в основном с http.

      P.S. Если не сложно, то расскажите, с чем вы поработали(библиотеки, фреймворки) пока изучали го, просто интересно.


      1. VanKrock
        26.08.2022 16:50
        +10

        Тут речь скорее об enterprise с большим доменом, у C# есть EntityFramework для этого, исключения, generic (с недавних пор и в go), MediatR, Automapper и многое другое. В go всё таки сложнее работать с доменами


        1. megasuperlexa
          26.08.2022 18:09

          с другой стороны, наличие MediatR может служить индикатором что на этот проект устраиваться не надо ))


          1. VictorNS
            26.08.2022 19:56
            +2

            можно спросить почему?


            1. dopusteam
              28.08.2022 11:16

              Потому что никто доходчиво не может объяснить, какую проблему он решает, усложняя навигацию по коду


          1. MagMagals
            26.08.2022 23:17
            +2

            буквально недавно на несколько каналах разработчика MAUI и веб сервисов, показывали, как с помощью MediatR делать и дестопные приложения или использовать его в miniAPI, что как бы дает намек что библиотека очень даже крута и ей надо пользоваться. может вы неправильно готовили её?


        1. Vadim_Aleks
          27.08.2022 00:25
          +3

          EF, MediatR, AutoMapper, DI, побольше рефлексии для входных аргументов обработчика (например, для валидации) и ваше приложение отвечает 200мс на обычный crud. Не для всех приложений такое позволимо, но об этом не задумываются при разработке MVP. "железом закидаем", "больше времени в БД проведём". Проблема c# как раз в таком подходе - слишком развязаны руки у разработчиков. Слишком хочется пользоваться уже написанным, не зная как оно работает внутри. Поэтому на Go и переходят в хайлоад проектах, как мне кажется.


    1. aegoroff
      26.08.2022 17:23
      +8

      Go больше подходит для системного программирования

      Не очень согласен - системное обычно там где нет рантайма, т.е. прямо делаем код который будет работать на голом железе. Возможно с Rust спутали. Хотя если считать k8s или Docker системами, то наверно может быть.


  1. Myclass
    26.08.2022 18:26
    +18

    На шарпе я проработал два с половиной года.

    Вы не обижайтесь, но за такой промежуток времени вы не можете проникнуться никаким языком. Да, пробежать по основным принципам - можно. Ткнуться в пару вещей - да, это вполне возможно, но чтобы так громко заявлять, что вы с шарпа на go переходите - это не совсем отражает то, что на самом деле произошло. А произошло следующее. Вы занялись шарпом. И через какое-то время перешли на другой язык. И теперь вам захотелось этот ваш шаг 'официально' подтвердить, что вы всё сделали правильно. Конечно-же найдётся в go что-то, что делается или лучше или проще. Вы на это и указываете. Имеете полное право, но шарп огромен, чтобы вот так на двух примерах его оценить - как-то однобоко.


    1. Sitro23 Автор
      27.08.2022 00:23
      +1

      Вы совершенно верно подметили посыл статьи: "В каждом языке есть то, что делается или лучше, или проще, чем на другом." Моя статья обзорная, потому что различий и общих мест у Go и C# масса. На их описании можно составить целую книгу. Если бы у меня была возможность, я с радостью написал бы что-то фундаментальное вроде "C# vs Go".


      1. Myclass
        27.08.2022 01:22
        +7

        Я вас понимаю, но немного приведу пример. Давайте поменяем шарп на французский язык, а go на английский. Итак ваш рассказ звучит след. образом.

        Как только я приехал во Францию, то сразу начал изучать французский язык, но почти сразу-же записался и на курсы английского языка. Вот уже прошло два с половиной года, я закончил курсы английского, нашёл работу, где говорят только на английском языке и всё- я решил поехать жить в Англию. И вот вам пример, что английский лучше, чем французский. Вы только представьте как говорят на французском число 90 - это quatre-vingt-dix . Это значит 4 раза по 20 + 10. С ума сойти можно. А теперь посмотрите как это классно и лаконично на английском - ninety.

        Думаю вас это улыбнуло. Вот так-же это воспринял и я, после вашего рассказа.

        60, 70, 80, 90 и другие числа во французском может быть и правда не очень практично сконструированы, но от этого песни Эдит Пиаф, Шарля Азнавура, Джо Дассена, Мерри Матьё, Патрисии Каас, Alizee, Zaz и многих других от этого ведь не хуже.


        1. Sitro23 Автор
          27.08.2022 14:29
          +1

          "Go лучше шарпа" - не тезис моей статьи. Пример с языками конкретный (на английском быстрей считать десятками), а выводы - огромные ("английский лучше, чем французский" и французская музыка хуже английской). По аналогии можно вывести, что из-за многознаковости римских цифр латинская литература хуже арабской.


    1. Vadim_Aleks
      27.08.2022 13:54
      +5

      за такой промежуток времени вы не можете проникнуться никаким языком

      Что значит "проникнуться"? Знать детали реализации рантайма? Понимать как писать идиоматичный код?

      И из вашего же сообщения:

      вы с шарпа на go переходите - это не совсем отражает то, что на самом деле произошло

      А произошло следующее. Вы занялись шарпом <...>, перешли на другой язык

      то есть, человек перешел с шарпа на го. В чем автор не прав?)


      1. Myclass
        27.08.2022 18:16
        +3

        Что значит проникнуться? Ну скажем так - когда мы говорим шарп, то уверен, что говорим не только о структуре и грамматике языка - это обьясняется быстро. Ну заучивается тоже в нормальное время. Мы говорим о шарпе и о многих бибоиотеках, которые нужны для работы в различных сферах. О различных структурах данных и работе с ними. Когда я вижу пример, где вставляется delay, не важно где - шарпе или в с++ для микроконтроллера, то почему-то думаю, что это не продвинутый подход к решению вещей. А именно начинающий. Может быть я ошибаюсь. Вполне.

        Насчет прав/не прав. Автор во всем прав. Если он считает, что он спец по шарпу и перешёл на го - его право. Если он решил об этом сообщить - тоже его полное право. Моё же право считать, что за небольшой отрезок времени, в те два с половиной года, где он каждый день больше занимался на курсах с го, а не интенсивно утонул в шарпе, что в шарпе он не мог стать спецом. Конечно, талант никто не отменяет. В жизни всякое бывает. К автору не отношу. Но бывает и такое, когда, книжки типа ,шарп за 21 день' дают людям представление, что это и правда возможно. Но придерживаюсь правила, что для познания каких либо знаний, как в человеческих языках, так и в языках программирования, в карате или в шахматах, в физике или в математике итд. нужно полное погружение, уйма проб и ошибок и время. Много времени. Может быть и тут ошибаюсь. Кто знает?

        Ладно - останемся каждый со своим мнением.


        1. koplenov
          27.08.2022 19:26
          +2

          Делал как-то гошный проект на работе

          Сходу для себя ощутил острую нехватку перегрузки функций и опциональных аргументов функций

          В чатике по го мне сказали что я ошибся языком, ¯\_(ツ)_/¯

          Что эти ваши дженерики, тут бы простой код писать без кучи дублирований

          Для себя я не нашёл жизни на го после шарпов


          1. Strafe2108
            27.08.2022 20:07
            +1

            Когда переезжал с php на go были такие же ощущения, но со временем это развеивается и с идиоматикой go начинаешь писать по-другому. Не стоит привязываться к конкретным фичам яп, это всего лишь инструмент. Та же аналогия из комментария выше - "я не говорю на французском из-за того,что там нет нескольких слов, которые мне нужны/нравятся". Попробуйте переосмыслить то, что используете на вашем яп и как это можно спроецировать на другой, откроете для себе много нового и интересного. Да, это выводит мозг из зоны комфорта, но это определенно улучшает восприятие и понимание того, что вы делаете.


            1. koplenov
              28.08.2022 19:12
              +1

              Хочется отстоять романтику своего утверждения:

              Я завязываюсь не на фичи языка, а на концепции, реализуемые языком

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

              Но плохо не потому, что это есть, а плохо потому, что мне, как программисту, это недоступно

              Когда меня убеждают в ненужности подобного, у меня словно отнимают инструменты, взгляды, парадигмы и концепции размышлений. Вся та сборная солянка, на которую можно гибко опираться при решении разного рода задач.

              Ограниченный скоп этого вы назвали идиоматикой языка, соглашусь.

              В своих эмпирических доводах опираюсь на подобную статью подобного рода:
              http://rsdn.org/article/nemerle/Amplifier.xml

              P.S. на практике не ограничиваюсь одним языком


          1. nikolas78
            27.08.2022 20:26
            +2

            Не то, чтобы я сильно топил за Go, но я рад, что есть ЯП без перегрузок функций.


            1. Sitro23 Автор
              27.08.2022 20:34

              Чем не устраивает перегрузка функций?


              1. nikolas78
                27.08.2022 21:13

                Тем, что затрудняет анализ ошибок в рантайме. И повышает требования к разработчику.
                Я вообще, знаете ли, жду появления процедурного ЯП с простым синтаксисом, с небольшим набором инструкций, с простыми структурами данных. Вся эта избыточная мощь (С++), лапша ООП и прочие UB уже немного надоели.
                Хочется понимать происходящее в месте где читаешь код, а не держать в голове кучу влияющих сайдэффектов.


                1. a-tk
                  27.08.2022 21:59

                  Ассемблер что ли?


                  1. viordash
                    28.08.2022 01:43

                    все сравнения в ассемблере построены на сайд-эффектах


                1. 0xd34df00d
                  27.08.2022 23:37
                  +2

                  а не держать в голове кучу влияющих сайдэффектов

                  Вам поможет хаскель.


            1. a-tk
              27.08.2022 21:47
              +1

              А ещё перегрузки нету в Си (который без плюсов).


  1. Sveta-Sun
    27.08.2022 06:43
    -2

    ????


  1. opv88
    27.08.2022 20:41

    Думал о полном переходе с шарпа на go несколько месяцев назад. Благо, писал в свое время на разных языках и go активно пиарили. Сделал пару pet-проектов, чтобы хотя бы примерно понимать, что это такое и с чем его едят, и отказался от этой затеи. Взяться за разработку чего-то масштабного на go не решился бы. Понятно, что язык проектировался под высоконагруженные масштабируемые crud'ы, но реализовывать там сложные алгоритмы работы с данными не стал бы.

    Впрочем, каждому свое. По факту, язык лишь одно из средств решения проблем. Все всегда упирается в известную диаграмму "быстро - дешево - качественно".


  1. LabEG
    27.08.2022 22:38

    Однако многопоточная обработка с обменом сообщениями между потоками на Go пишется быстрее и получается лаконичнее. 

    А можно поподробнее про практическую часть этого утверждения? По собственному опыту современный шарп выстроен таким образом что тебе в принципе нету необходимости заниматься перегонкой данных между потоками, там либо потоки так организуются что этого не требуется вовсе в 99% случаев, либо используются конкурентные коллекции которые решают оставшийся 1% случаев.


    1. Sitro23 Автор
      27.08.2022 23:55

      На практике также не приходилось использовать каналы в шарпе. Однако они есть в языке, а статья основана на сравнении C# и Go. Поэтому пример с каналами не мог не привести. К тому же он отлично подтверждает тезис: "На C# можно решать любые задачи, причём зачастую несколькими способами."