Pull to refresh

Comments 87

За наводку на GoMetaLinter отдельное спасибо!
UFO just landed and posted this here
Я не очень понял проблему по описанию.
Могу посоветовать зайти к нам в слак- чат, и там спросить — будет продуктивнее :)
UFO just landed and posted this here
Слак — это не очередной мессенджер, в котором ты можешь «быть или не быть», — это чат комната с обязательной регистрацией.
Вот такие статьи по Go и нужны. Ничего против не скажешь.

ООП — это отнюдь не «классы» и «наследование»

Как раз недавно в комментариях в статье про Swift вылезла эта тема. Go и без привычного наследования является ООП языком, а структуры Go вообще не вижу причин не называть классами. Да, есть явно разделение на интерфейс и свойства, но все вместе это дает вполне себе классы — мы создаем объекты, которые имеют методы для работы с ними. Вроде все как у всех. Ну и полиморфизм, что важно, на месте. После протоколов Obj-C и интерфейсов C# все это выглядит вполне знакомо.
UFO just landed and posted this here
Вот такие статьи по Go и нужны. Ничего против не скажешь.

Присоединяюсь! Таким, как эта статья, хотелось бы видеть каждое краткое введение в язык.
Go и без привычного наследования является ООП языком, а структуры Go вообще не вижу причин не называть классами.
Серьезно? Или вы спать будете плохо, если окажется что Go не объектно-ориентированный язык? Так вот, в Go нет ООП. В нем нет ни классов, ни наследования, ни прототипов, ни полиморфизма. Go – не объектно-ориентированный язык. Извините.
Существуют более-менее общепринятые признаки языка с поддержкой ООП. И Go не подходит под эти признаки. Это не значит, что на Go нельзя писать объектно-ориентированный код. Но с таким же успехом объектно-ориентированный код можно писать и на C, Haskell и Rust.

И да, нет ничего разумного в том, чтобы придумывать собственные определения для уже существующих терминов. Уж очень получается похоже на «Я считаю Go объектно-ориентированным, потому что мне так очень хочется».
И что? Такие ответы в faq пишутся в основном для тех, кто считает что если язык не объектно-ориентированный, то на него даже смотреть не стоит. «Go можно считать объектно-ориентированным языком, потому что у нас есть типы и функции, правда нет иерархии классов, и классов тоже нет». С таким успехом Pascal — тоже объектно-ориентированный язык.
Просто FYI, что этот вопрос уже даже рассмотрен в офф. документации, и не стоит за это минусовать мой коммент.
И там первым же предложением пишут «Yes and no» с пояснением.
Я не против, но если вы публикуете ссылку на что-то, предполагается, что вы полностью согласны с тем, что там написано.

В ответе написано, что на Go можно писать объектно-ориентированный код. С этим я полностью согласен. Но на прямой вопрос «является ли Go объектно-ориентированным языком» дан ответ «и да и нет», что лично я считаю лукавством, потому что правильный ответ «нет». И причины этого лукавства находятся скорее в плоскости маркетинга, чем в плоскости computer science.
В этом я с вами согласен, но вот смотрите как выходит — на Go можно писать объектно-ориентированный код (т.е. в языке есть механизмы для реализации соотв. вещей или их аналогов). Но при этом Go не объектно-ориентированный язык в классическом смысле. Как тут еще можно сказать, кроме «и да и нет»?
Если бы ответ писал я, я бы написал так: «Нет, но это не мешает вам писать на Go в объектно-ориентированном стиле. Для этого в Go существуют такие-то механизмы...» и дальше по тексту.

В целом, я придираюсь к ответу только потому, что из-за его формулировки люди могут сделать неправильные выводы. А с сутью ответа я полностью согласен.
Если что-то общепринято, оно не становится по умолчанию правильным. Алан Кей, создатель ООП, как-то сказал:
«Я придумал термин «объектно-ориентированный», и я уверяю вас, что не имел в виду C++».

Так кто из нас придумывает собственные определения существующих терминов? Ваши придирки напоминают мне споры о том, есть ли классы в javascript'e. От того, что вместо «type Circle struct» будет написано «class Circle», язык не станет вдруг более оопэшным. И наоборот.

И да, нет ничего разумного в том, чтобы следовать букве определения, игнорируя его дух. Уж очень получается похоже на человека по Платону.
Ну да, ведь когда Алан Кей придумал термин «ООП», он на самом деле имел ввиду Smalltalk. :)

Вы не вполне правы по поводу «моих придирок». Я просто хочу докопатся до сути. Мне нет дела до конкретных названий, будет там «class» или «struct», «interface» или «trait» или «abstract class». И я, так же как и вы, готов следовать «духу» а не «букве».

Но есть определенные вещи, которые отличают ООП-подход от просто хорошего дизайна структурного кода. И самая главная такая вещь – это возможность наследования (будь оно прототипным или нет), потому что такие понятия как «полифорфизм», «инкапсуляция» и «абстрактные сущности» не являются эксклюзивными для ООП.
Полиморфизм через подтипирование (subtype polymorphism) встречается в не-ООП языках?
Википедия определяет «subtyping» и «subtype polymorphism» как одно и тоже. Что вы имете ввиду?
Извините, я не понял к чему это утверждение, но повторю вопрос, котрый видимо был непонят. Я спрашиваю частный вид полиморфизма, а именно полиморфизм через подтипирование, встречается ли где либо кроме ООП языков.
«Полиморфизм через подтипирование» — это тоже самое что и «наследование». Я не знаю такие не-ООП языки программирования, в которых можно встретить наследование. Я ответил на ваш вопрос?
Хорошо, в данном случае этот вид полиморфизма все же является понятием эксклюзивным для ООП :)
Что никак не противоречит моему изначальному утверждению: «полиморфизм не является эксклюзивным понятием для ООП».

Потому что есть еще параметрический полиморфизм, ad-hoc полиморфизм и т.д., которые не специфичны только для ООП.

В то же время я сказал, что «есть определенные вещи, которые отличают ООП-подход от просто хорошего дизайна структурного кода. И самая главная такая вещь – это возможность наследования», что означает именно то, о чем вы говорите.
Если судить по цитатам Алана, то больше всего под его описание подходит Erlang (ну, а теперь и Go). :)

Прочитав ваш комментарий ниже, я понял что, возможно, не слишком хорошо понимаю ооп в «общеупотребительном» смысле. Что композиция, что наследование — в некотором смысле это схожие подходы, которые применяются для решения схожих или даже одинаковых проблем. Почему наследование считается ооп-инструментом, а композиция — нет, для меня загадка. И в том и в другом случае у нас есть структура/класс, которая, грубо говоря, расширяется за счет одной или нескольких других структур.
К слову, Go предлагает заменить наследование композицией. Но композиция – это как раз тот инструмент, который используется в структурном подходе для написания расширяемого кода. Так что врядли это можно назвать следованием «духу» ООП.
Вы с языком то знакомы? Полиморфизм достигается через интерфейсы, при этом голыми структурами вообще не принято оперировать. fmt.print функции вкупе с интерфейсом Stringer отлично это используют. Классы, а что есть классы, что их там нет? Наследование? Embedding позволяет «наследовать» все методы, интерфейсы и поля исходного типа, что из-за этих самых интерфейсов позволяет получить в этом случае полиморфизм. При этом есть приватные и публичные поля и методы. Все как по ООП. Мне понятно, что вы цепляетесь к определениям, которые заучили в школе и универе. И Go действительно не ложится на них идеально, но концепции, которые в них заложены, он реализует, а значит и может называться ООП языком. Просто это не C# и Java, вот и все.

Спать я буду спокойно, но меня удивляет такая вот вера в непоколебимость определений ООП, за которыми стоят не концепции, а не какие-то конкретные реализации в языках. Мне вот хочется называть его ООП, потому что таково мое мнение, я вижу концепции ООП в таком свете. Вам так не кажется — можете придумать для Go какие-то другие определения. Не суть важно.
«Полиморфизм достигается через интерфейсы.» Наверное все-таки не полиморфизм, а dynamic dispatch? Я имел ввиду именно параметрический полиморфизм (или generics).

У вас в голове как-будто смешаны в одну кучу понятия наследования, полиморфизма и композиции (embedding). Можно ли их заменять друг другом? Можно, но это уже не будет наследованиям. Выше я уже писал, что композиция появлиась за долго до того, что было придумано само понятие ООП и использовалось повсевместно в структурном коде. Так что по-поводу «Все как в ООП» вы строго не правы.

Я не цепляюсь к определениям. Я еще своей головой умею думать. И я хочу что бы понятие ООП было именно понятием, а не маркетинговым buzz word и ярлыком, которой вешают на все что должно блестесть. Вы же наоборот размываете понятие ООП до такой степени, что вообще любой язык можно назвать ООП. В чем тогда вообще смысл такого понятия? Чтобы блестело?
Да, я размываю определение ООП до степени концепций, а не конкретных реализаций в любимых языках. Смысл этого? Не знаю, просто дискуссия. Поднята тема ООП в Go, я высказал свое мнение на этот счет. Если почитаете интернеты, то эта тема частенько поднимается. Люди пытаются найти в Go ООП, которое они видели в других языках. В ответ же звучит — Go реализует концепции ООП, но не так, как это делают другие языки. В конечном итоге важны именно концепции и их практическая польза, а не четкость понятий, которые живут только в академических кругах. Вам это сильно не нравится и вы зачем-то мне пытаетесь что-то доказывать. Еще раз, мне не важны определения. Мне важные концепции и в вопросе ООП, которые подняла статья, я имею уже упомянутое мнение.

Что до параметрического полиморфизма. Пожалуйста, кладите в список интерфейсы и вы получите то, что вам надо. Я не понимаю, вам так важно, чтобы все выглядело как в каких-то языках или все таки удовлетворялись идеи определенные? Основная идея полиморфизма — положил кучу объектов не важно какого типа, но реализующих единый интерфейс, в один массив. Все, ты работаешь с ними единым образом. Это полиморфизм и никакие generics тут не нужны. Objective-C как-то прекрасно живет на позиции ООП языка (даже частенько называет более православным, чем С++), но в нем generics нет (недавно появились всего лишь аннотации для компилятора). Это не говоря о том, что вы зачем-то ограничились одним узким примером полиморфизма, да еще дополнительно ограничили его до дженериков.
У меня нет «любимых» языков или привычных для меня реализаций, я довольно много совершенно разных подходов видел. Мне все равно, как синтаксически это будет выглядить. У меня нет сомнений в том, что на Go можно писать объектно-ориентированный код. Впрочем, как и на любом другом языке. Делает ли это язык объектно-ориентированным? Нет.

Если вам не важны определения, то что вы мне, простите, хотите доказать? Мне важны определения. Давайте на этом и закончим.

Мне вот хочется называть его ООП, потому что таково мое мнение, я вижу концепции ООП в таком свете.
Это такой шедевр, что я даже не знаю, что вам на это ответить. Вы точно правильно профессию выбрали? Вы инженер или литературный критик, чтобы использовать аргумент в стиле «я художник, я так вижу»?
«Полиморфизм достигается через интерфейсы, при этом голыми структурами вообще не принято оперировать.»

Это кстати интересно. Получается что статическая типизация коту под хвост?
Но оперируем же мы не пустыми интерфейсами (по сути, аналог object), а конкретными типами. Статическая типизация. Просто как-то принято наружу из библиотек выставлять не структуры, а интерфейсы. Вы открыли сокет — вам наружу интерфейс, статический определенный тип. Зачем вам знать, что за структура за ним стоит? Кажется испокон веков такое используется в ООП языках и когда я начал писать на Go мне сразу вспомнилось — да это же как интерфейсы и протоколы в C# и obj-C соответственно. Прям вспоминаются все эти умные книжки, где талдычат — везде наружу выставляем интерфейсы. Ибо слабое сцепление и прочие умные концепции.
Чем это принципиально отличается от PyObject или NSObject? Это динамическая типизация, а не статическая (пусть и строгая).

«Выставлять наружу только интерфейсы» оправдано только тогда, когда язык не поддерживает параметрический полиморфизм. С параметрическим полиморфизмом включается механизм мономорфизации, позволяющий заменить dynamic dispatch на static dispatch. Соответственно, прятать все за интерфейсы нет необходимости.
у меня возник вопрос. даже два :)

1) Как принято в большом go проекте быстро понимать реализует ли «класс» какой-то интерфейс? Смотреть все 10-20 методов класса, пытаясь найти все 5, которые должны быть в интерфейсе?
2) как работают с сериализацией в го? где можно тонкости стандартных подходов посмотреть?
По первому можно ответить словами самих авторов. В Go принято и всячески рекомендуется делать интерфейсы из минимального количества методов вплоть до одного. Тогда проблема уходит само собой. С другой стороны, такой проблем по идее и не должно происходить. Есть метод, есть аргументы — сразу понятно, что он хочет. Библиотеки обычно возвращают интерфейсы, а не голые структуры, что, опять же, снимает эту проблему. А так да, только смотреть на список методов. Иных механизмов язык вроде бы не предоставляет, хотя помощь IDE здесь была бы все таки уместна.
1. godoc со специальным ключиком сгенерирует документацию по коду, которая по каждому интерфейсу напишет список реализующих его типов. С блекджеком и гиперссылками.

2. Сериализацию можно или делать из коробки, дописывая аннотации к полям структур, как они должны называться на вводе-выводе, а потом используя стандартные пакеты encoding/json, encoding/xml, или использовать protobuf со своими плюсами и минусами. Это что часто используется.
1) Обычно это не нужно «понимать», потому что это понимает компилятор — если переменная этого типа где-то используется как интерфейс (передается в функцию, принимающую интерфейс, или кастится к интерфейсу), то компилятор выдаст ошибку, если тип не реализует интерфейс. Вот прямо так и скажет:
cannot use T as MyIface… T does not implement MyIface (missing SomeMethod method)


Если же тип нигде и не используется как интерфейс, то, собственно, и надобности нет.
Теоретически, может быть специфическая ситуация, когда нужно все таки заставлять компилятор делать проверку (это может быть полезно в какой-то плагин-подобной архитектуре, например), то можно просто сделать кастинг к интерфейсу и этим гарантировать compile-time check:
var _ MyIface = (MyType)(nil)

Но я такой хак видел только в статьях, на реальном коде, пожалуй, и не видел.
Ну или тестами это можно «гарантировать».
Кстати, на смежную тему — есть утилита impl, которая позволяет быстро сгенерировать код функций-пустышек для нужного интерфейса.

2) в стандартной библиотеке — посмотрите всю папку encoding.
Если надо много и часто сериализовать советую смотреть на ffjson или messagepack и кода генерация для этого. Можно получить очень большой прирост в скорости. стандартная сериализации получает interface поэтому должна искать что как сериализовать. Кода генерация даёт методы заточеные для страктов и поэтому очень быстро работает
Я так понимаю такой крутой расчет площади ради упрощения, чтобы float64 не вводить везде?
func (c Circle) Square() int64 {
	return (2 * c.Radius) ^ 2
}

Переопределения площади для RoundRectangle по той же причине нету?

Простите за занудство, но все же ввести float64 ради нормальных расчетов было бы не большим усложнением.
Согласен, спасибо за замечание. Старался сделать кальку с оригинального примера, но, и вправду, поправить эту оплошность ничего не стоит.
Тогда и площадь для rounded rectangle добавьте, чтобы всё честно было :)

S = a*b + (4 - pi)*r^2
Тогда уж
func (r RoundRectangle) Square() float64 {
	return r.Rectangle.Square() - (4 - PI)*r.RoundRadius^2
}

Предполагается, что все имеет тип float64.

Кажется у вас ошибка:
S = a*b - (4 - pi)*r^2
Да, минус конечно же :)
Не надо в полчетвёртого утра писать комментарии...
Ну и это, в Go оператор ^ — это XOR :)
Оу! Вот это я лопухнулся! Спасибо, поправил. Вот еще думал, писать в «финальных правках» о надобности тестами покрыть, или нет )
package main

import (
	"math"
	"testing"
)

func TestCircle(t *testing.T) {
	square := NewCircle(0, 0, 10).Square()
	want := 100 * math.Pi
	if square != want {
		t.Fatalf("Want %v, but got %v", want, square)
	}
}
UFO just landed and posted this here
Месяц назад ломал голову над примерно такой проблемой:
type Figure interface {
	...
	AboutSquare() string
}

type Rectangle struct {
	...
}

func (r RoundRectangle) Square() float64 {
	return math.Abs(float64((r.Right - r.Left) * (r.Top - r.Bottom)))
}

func (r Rectangle) AboutSquare() string {
	fmt.Sprintf("my square: %d", r.Square())
}

type RoundRectangle struct {
	...
}

func (r RoundRectangle) Square() float64 {
	circSquar := math.Pi * float64(r.Radius^2)
	rectSquar := math.Abs(float64((r.Right - r.Left) * (r.Top - r.Bottom)))
	return rectSquar - float64((r.Radius*2)^2) + circSquar
}

func newRoundRectangle(...) *RoundRectangle {
	...
}

myRoundRectangle := newRoundRectangle(4, 4, 2)
fmt.Println(myRoundRectangle.AboutSquare()) // my square: 16

А хочется, чтобы myRoundRectangle.AboutSquare() возвращал 12.566370614359172. Ну не генерировать же для каждого типа свой метод/функцию AboutSquare.
Решил так:
type Rectangle struct {
	this *Figure
}

func newRoundRectangle(...) *RoundRectangle {
	ret := return RoundRectangle{
		...
	}
	ret.this = &ret
	return &ret
}

func (r Rectangle) AboutSquare() string {
	fmt.Sprintf("my square: %d", r.this.Square())
}


, но чует мое сердце, что я не прав.
sry, * func (r Rectangle) Square() float64 {
return math.Abs(float64((r.Right — r.Left) * (r.Top — r.Bottom)))
}
Можно и так:
func AboutSquare(r Figure) string {
	fmt.Sprintf("my square: %d", r.Square())
}
Можно, но еще хочется, чтобы можно было переопределить на одной из цепочек наследования этот метод (AboutSquare)
func (r RoundRectangle) AboutSquare() string {
	fmt.Sprintf("my square: %d +- 10^-20", r.Square()) // +- - т.к math.Pi
}
Ну тут и вправду незачем делать AboutSquare методом, правильней будет сделать функцию, принимающей параметр Figure.
Ну или определите AboutSquare для того типа, который встраивает, а не который встраивается (для RoundRectangle, в примере).
return &RoundRectangle{
*NewRectangle(left, top, right, bottom),
round,
}

Тут вы нарушили инкапсуляцию, завязав публичный интерфейс на способе агрегации. Есть несколько способов агрегации и их периодически придётся менять:
Прямоугольник + радиус
Прямоугольник + 4 радиуса
Прямоугольник + 8 радиусов
4 круга
4 элипса
8 точек
16 чисел

Вообще говоря, наследовать одну фигуру от другой — типичная ошибка ООП. Так что такую «типичную задачу ООП» и решать не стоит.
наследовать одну фигуру от другой — типичная ошибка ООП


тяжело такое слышать :) можно поподробнее?
Подозреваю, что речь про https://en.wikipedia.org/wiki/Circle-ellipse_problem или, в более общем случае, https://en.wikipedia.org/wiki/Liskov_substitution_principle
Если я правильно понял, то проблема получается если мы оперируем объектами типа «круг» («квадрат») с расчетом на особенности их поведения (при изменении радиуса (высоты) изменятся линейные размеры по обоим осям, но это не верно для «элипса» («прямоугольника»)).
Но если мы оперируем исключительно базовыми классами «фигура», то эта проблема не возникнет.

Похожу проблему описывал Скотт Майерс в «Эффективное использование С++»:
www.e-reading.by/chapter.php/1002058/80/Mayers_-_Effektivnoe_ispolzovanie_CPP.html
Когда делается предположение, что птицы умеют летать, пингвин — птица и значит класс пингвин может быть наследником класса птица. Но что в этом случае делать с методом fly класса птица. :)
Было бы классно увидеть подобный разбор написания тестов на go.
А вот как это решается на языке D: http://dpaste.dzfl.pl/539fd2b9c850
К area, я добавил ещё габариты, координаты цента, для координат выделил отдельный класс точек, ну и покрыл всё это дело тестами.
Хочется отметить, что existential quantification тут используется, но идёт оно из коробки. Инженерный смысл данного расширения в том, что мы таскаем словарь вместе с типом данных в рантайм (что я так понимаю делается в Go всегда), т.е. позволяет делать массив чего-то, что является instance of Figure. В Haskell классы типов (которые чуть-чуть похожи на instance) разрешаются в compile time, но существуют случаи когда этого не достаточно, как в этом примере
Хорошая статья. По делу. Спасибо.

Странно, правда, что первое движение после того, как код готов — написать коментарии, а не тесты.
Да и коментариий тоже. Но коментарии в конце написаны, а тесты таки нет.
Я стараюсь тесты писать по мере написания кода. Если отложить «на потом», то. как правило, до них дело не дойдет. Тут же пример был несколько искусственный — фактически, переносил код с другой статьи. Подумал про тесты, но решил, что для статьи не релевантно. За что потом словил баг в комментариях и пожелал. :)
Кто-то уже в коментариях высказался — надо статью про тестирование в Go.
Спасибо за статью.
Есть пара вопросов по указателям:
Общее правило тут такое — если метод должен изменять значение c или если Circle — большущщая структура, занимающая много места в памяти — тогда использовать указатель. В остальных случаях, будет дешевле и эффективней передавать значение в качестве ресивера метода.

С этим более-менее понятно, хотя как понять насколько у меня «большущая структура», что пора использовать указатель?

func NewRectangle(left, top, right, bottom int64) *Rectangle

Почему конструктор возвращает указатель на структуру? Ведь структура Rectangle не большая, не выгоднее ли возвращать саму структуру?
Всегда ли принято, что конструктор возвращает именно ссылку или нет?

Почему конструктор возвращает указатель на структуру?

Ну а что ещё вернуть конструктору. Он создаёт новый объект, которого до этого не было. Возвращать саму структуру, как я понимаю, значит лишний раз делать её копию без получения каких-либо плюсов.
Вы случайно не путаете copy-on-write с move semantics?
Не путаю. COW — просто более общий случай, который не требует какой-то отдельной семантики (хотя семантические подсказки порой необходимы). COW правильнее было бы называть copy-when-necessary, но так уж прижилось.
Тогда давайте обсудим.

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

Move semantics — это когда вместо того, чтобы копировать содержимое области памяти, на которую ссылается переменная, в новую переменную, туда передаётся ссылка на старую область памяти. Старая переменная после этого считается не подлежащей к использованию. Копирования нет.

Возникает 2 вопроса.
1. Как move semantics может быть общим случаем для COW, если move semantics копирования не предполагает?
2. Как COW может устранить необходимость копирования созданного объекта, если там копирование есть по определению?
Это кривая реализация COW :-) Прямая выглядит так:
1. При копировании просто создаётся дополнительная ссылка
2. Если на область памяти есть более чем одна ссылка, то при записи происходит реальное копирование данных.
3. Если компилятор может понять, что по старой ссылке больше не происходит обращений (например, ссылка явно или неявно обнуляется), то он может безболезненно вырезать код отвечающий за подсчёт ссылок. Это и есть тот самый частный случай с перемещением данных.
Таким образом move semantics никак не может являться частным случаем COW, потому, что там переменную, из которой вытащено значение, использовать нельзя категорически.

Но вот насчёт COW интересно. В каких языках есть COW, реализованный правильным способом?
Как и к любую другую обнулённую переменную. Я не зря упомянул явное и неявное обнуление.

В PHP, например: http://habrahabr.ru/post/226707/
Спасибо за ссылку, ознакомился с COW в php. Если я правильно понял, для объектов COW в php не работает ибо в переменной содержится id объекта, а не сам объект. COW для примитивных значений в php это наверное разумно, так как они занимают в памяти много места. Но в Go эти значения занимают столько же или меньше, чем ссылки на них, поэтому тут COW может даже помешать.

Со строками, массивами и объектами, может это было бы полезно. Но подозреваю расплатой за такую штуку будет серьёзная просадка в производительности.
Возвращать саму структуру, как я понимаю, значит лишний раз делать её копию без получения каких-либо плюсов.

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

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

В доке об этом мало что написано, может кто поможет разобраться когда что использовать?
К примеру, рассмотрим стандартный пакет image (https://golang.org/pkg/image/#pkg-index)
Практически все типы пакета имеют конструкторы, которые возвращают указатель, и все функции написаны для ресивера-указателя.
Но есть пара типов (Rectangle, Point), конструкторы которых возвращают именно значение и все функции написаны для значения (естественно, go копирует их и для указателя).
Почему там так, и почему в этой статье конструктор аналогичный Rectangle-сущности из пактета image возвращает указатель?
Да, чаще всего вы возвращаете указатель. Отчасти он «более популярен», потому что сложные структуры инициализируются с помощью new(), который возвращает именно указатель. В Effective Go это рассмотрено.

При этом ничего не мешает возвращать реальную структуру. Это может быть выгодней, в определенных случаях. Но тогда лучше не называть метод New..., чтобы избежать путаницы. Пример с image.Rect как раз об этом — можно спокойно возвращать не ссылку, называть это не New.., и все будет замечательно (хотя, конкретно по image я не знаю, почему решили так, а не иначе — но это однозначно не принципиальный вопрос, а каких-то мелких удобств в коде)
хотя как понять насколько у меня «большущая структура»

Хороший вопрос. Как я понимаю, однозначного ответа на него нет, все попытки объяснить «насколько большой должна быть большая структура» сводились к простыням рассмотрения различных случаев. Но вот что написано в вики:
If the receiver is a large struct or array, a pointer receiver is more efficient. How large is large? Assume it's equivalent to passing all its elements as arguments to the method. If that feels too large, it's also too large for the receiver.

Если ресивер (метода) это большая структура или массиво, будет эффективнее использовать указатель. Насколько большая «большая структура»? Представьте, что вам нужно передать все её поля как параметры функции. Если это кажется слишком громоздким, значит это большая структура для ресивера.
Спасибо за ответы.
Я обычно исхожу из того, что преждевременная оптимизация — зло и лучше иметь некое единообразие в коде и удобство, а оптимизировать только тогда, когда это становится узким местом. Ведь маленькая структура может легко перерасти в большую, с развитием проекта, а это значит надо рефаторить все функции на использование указателя, вместо значения.

Из полученной информации могу сделать такой вывод — для структур всегда лучше использовать указатели, и только если они действительно очень маленькие (два-три свойства простого типа) и вряд ли будет расширяться с развитем проекта — тогда использовать по значению. Таким образом, для структур, которые, например, представляют собой модель из БД выгоднее всего использовать указатель всегда.

Все верно?
Про преждевременную оптимизацию — безусловно, верно. Но значения vs указатели — это не только про размеры. Например, если используется ресивер-значение, то этот метод автоматически безопасен для исполнения во многих горутинах. Если же ресивер — указатель, нужно думать о том, будет ли этот метод вызываться параллельно и будет ли меняться переменная-ресивер (и тогда уже защищать мьютексом).
Вот тут неплохо попытались расписать разные случаи: stackoverflow.com/questions/23542989/pointers-vs-values-in-parameters-and-return-values
В случае с конструктором рассуждения про безопасность для исполнения в горутинах ведь бессмысленны, да?
Ну да, выше речь шла про ресиверы и методы. А методы от функций отличаются лишь целесообразностью использования — метод подразумевает некоторый стейт, и операции с ним. А конструктор — это просто функция, которая создает новый объект, это безопасно для параллельного исполнения.
как понять насколько у меня «большущая структура», что пора использовать указатель?

На x86_64 указатель занимает 64 байта. Как только ваша структура начинает занимать более 64 байт имеет смысл использовать указатель. Если сформулировать по другому — указатель имеет смысл исползовать с момента, когда копирование указателя происходит гораздо быстрее, чем копирование структуры. «Гораздо» — в каждом случае своё. Иногда можно подождать даже милисекунду, потому что объекты создаются раз в минуту. А иногда подождать нельзя вообще, потому что объекты создаются непрерывно.
Sign up to leave a comment.

Articles