Если вы хоть раз были сбиты с толку, что означает символ "амперсанд" (&) или "звёздочка" ("знак умножения", *) или запутывались, когда что использовать, то это статья для вас. Авторы Go старались сделать язык знакомым большинству программистов, и многие элементы синтаксиса заимствовали из языка С. Но в 2017м уже сложно понять, большинство программистов владеют С или нет, и смею полагать, что уже нет. Поэтому концепции хорошо знакомые прошлому поколению разработчиков, могут выглядеть совершенной абракадаброй для для нового поколения. Давайте немного копнём историю и расставим все точки над ї в вопросах указателей в Go и использования символов & и *.



Указатели


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


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

Вот визуализация из статьи:
image


Здесь Point{10, 20} это "литерал" — новая переменная, объявленная на месте, "блок памяти", а & — это "адрес этого блока памяти".


Тоесть в коде:


var a int
var b = &a
fmt.Println(a, b) //  выведет "0 0x10410020"

переменная b будет является указателем и содержать адрес a.


Тот же код, но запишем тип b явно:


    var a int
    var b *int = &a
    fmt.Println(a, b) //  выведет "0 0x10410020"

здесь звёздочка означает "тип указатель на число". Но, если она используется не перед типом, а перед самой переменной, то значение меняется на обратное — "значение по этому адресу":


    var a int
    var b *int = &a
    var c int = *b
    fmt.Println(a, b, c)  //  выведет "0 0x10410020 0"

Это может запутывать и сбивать с толку, особенно людей, никогда, не работавших с указателями, которых нет, например, в таких популярных языках как JavaScript или Ruby. Причём в языках вроде C и С++ есть ещё масса применений указателям, например "арифметика указателей", позволяющая вам прямо смещениями по сырой памяти бегать и реализовывать невероятно быстрые по современным меркам структуры данных. Ещё очень удобно переполнение буфера получать благодаря этому, создавая баги, приносящие ущерб на миллиарды долларов. Есть даже целые книги по тому, как понимать указатели в С.


Но если механика работы с указателями в Go относительно простая, остаётся открытым вопрос — почему "амперсанд" и "звёздочка" — что это вообще должно означать? Возможно это потому что символы рядом на клавиатуре (Shift-7 и Shift-8)? Ну а чтобы понять любую тему, нет способа лучше, нежели копнуть её историю.


История


А история такова. Одним из авторов Go был легендарный Кен Томпсон, один из пионеров компьютерной науки, подаривший нам регулярные выражение, UTF-8 и язык программирования B, из которого появился C, на базе которого, 35 лет спустя, появился Go. Вообще, генеалогия Go немного сложнее, но С был взят за основу по той простой причине, что это язык, который десятилетиями был стандартом для изучения программирования в университетах, ну и о его популярности в своё время, думаю, не нужно говорить.


И хотя сейчас Кен Томпсон отошёл немного от Go и летает на своём частном самолёте, его решения проникли в Go ещё задолго до Go. В юности он развлекался тем, что писал на завтрак новые языки программирования (слегка утрирую), и одним из языков, который был им создан вместе с ещё одной легендой компьютерной науки Денисом Ритчи, являлся язык программирования B (Би).


В то время Кен Томпсон написал операционную систему на ассемблере для компьютера PDP-7, который стоил 72000 долларов — а это примерно полмиллиона долларов сегодня — обладал памятью в 9 КБ (расширялась до 144КБ) и выглядел вот так:



Собственно, эта операционная система называлась Unics, и затем была переименована в UNIX. И когда зашла речь о переписывании её для нового крутого компьютера PDP-11, было принято решение писать на каком-то более высокоуровневом языке программирования. BCPL, который был предшественником B был слишком многословен — много букв. B был более лаконичен, но имел другие проблемы, которые делали его плохим кандидатом для портирования UNIX на PDP-11. Именно тогда Денис Ритчи и начал работать над новым языком, во многом основанном на B, специально для написания UNIX под PDP-11. Имя C было выбрано, как следующая буква алфавита после B.


Но вернёмся к теме об амперсанде и звёздочке. Звёздочка (*) была ещё в языке BCPL, и в B попала с тем же смыслом обозначения указателя, просто потому что так было в BCPL. Ровно по этой же причине перекочевали в С.


А вот амперсанд (&), означающий "адрес переменной", появился в B (и также перекочевал в С просто потому что), и был выбран по нескольким причинам:


  • нужен был один символ, а не два или целое слово
  • выбор символов был очень ограниченный (об этом чуть ниже)
  • как говорит сам Кен Томпсон, слово "амперсанд" звучало мнемонически похоже на "адрес" и было выбрано именно по этому.

Если я вас запутал, то вот нагляднее:


И тут нужно посмотреть внимательно на клавиатуры того времени. Чуть выше на картинке PDP-7 можно рассмотреть вводное устройство, коим являлся Телетайп 33. Стоит посмотреть на его клавиатуру повнимательнее, чтобы понять реалии того времени, и понять, с какими ограничениями сталкивались программисты и дизайнеры языков программирования в то время:



Как можно увидеть, ни тачбара, ни эмоджи не было :), и символы приходилось выбирать только из того набора, который был в телетайпе. Также, примечательно, что амперсанд и звёздочка тогда были не рядом, а на целых 4 клавиши порознь, что опровергает идею выбора амперсанда из-за близости клавиш. Собственно, из всех доступных клавиш, Кену Томпсону на тот момент больше всего приглянулся "амперсанд", похожий на "адрес".


Ну а дальше вы знаете — С стал языком века (прошлого), повлиял на огромное количество других языков, а книги по С стали настольными библиями программистов на несколько десятилетий. В таком же виде указатели вместе со звёздочкой и амперсандом попали и в С++ — ещё один язык мейнстримовый язык, на котором до Go писалась большая часть сетевого и серверного софта.


Поэтому решение включить указатели (без арифметики указателей, к счастью) в Go с тем же синтаксисом — было вполне логичным и естественным. Для С/C++ программистов это такие же базовые и простые понятия, как скобочки { и }.


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


Заключение


Если вы всё ещё неуверенно себя чувствуете себя с указателями в Go, запомните два простых правила:


  • "Амперсанд" & звучит похоже на "Адрес" (ну, и то слово и другое на "А" начинается, по крайней мере)))), поэтому &x читается как "адрес переменной X"
  • звёздочка * ни на что не похоже на звучит, но может использоваться в двух случаях — перед типом (var x *MyType — означает "тип указателя на MyType") и перед переменной (*x = y — означает "значение по адресу")

Надеюсь, кому то это немного поможет лучше понимать смысл указателей и символов, стоящими за ними в Go.

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


  1. VaalKIA
    03.10.2017 05:03

    А зачем вообще для типов и для переменных использовать разные символы, в Паскале и там и там используется "^" и всё работает, да и в B, вроде бы тоже, достаточно было только звёздочки (по мне, так достаточно одного амперсанда, раз уж звёздочка это умножение для переменных). Так что такое глубокое умозаключение про амперсанд, при введении двух знаков одновременно, мне вообще не понятно, ибо для меня вопрос «нафига» остаётся не раскрытым.


    1. flatscode
      03.10.2017 05:24
      +3

      Ничего не перепутали?

      В Паскале используется "&" для оператора взятия адреса, "^" для определения ссылочного типа, "^" для взятия значения по ссылке.

      В С используется "&" для оператора взятия адреса, "*" для определения ссылочного типа, "*" для взятия значения по ссылке.


      1. geher
        03.10.2017 07:46
        +1

        В Паскале используется "&" для оператора взятия адреса,

        Вообще-то '@'.


        1. flatscode
          03.10.2017 07:58

          Да, верно, ошибся с копипастом :-)

          Но вопрос тот-же про «и там и там используется ^», что имеется ввиду?


          1. MacIn
            03.10.2017 09:35
            +1

            Ошибка. Символ каретки в Паскале так же используется, как и звездочка в сях, в зависимости от позиции — перед переменной/перед типом для разыменовывания или задания типа. В Паскале каретка до — тип, после — разыменовывание:
            PMyType = ^TMyType - указатель на TMyType
            var
            abc: PMyType;
            ...
            := abc^.field;


      1. VaalKIA
        03.10.2017 08:53

        Ах, это Addr() ("@" я не использую), я просто не понял о чём речь, мне подумалось, что там разные знаки: для указания, что это ссылочный тип и для работы с переменными ссылочного типа.


        1. MacIn
          03.10.2017 09:37

          В Си, кстати, звездочка намного логичнее — объявление вида «вот такое получится, если разыменовать», а в Паскале — нет, мнемоники никакой нет.


          1. flatscode
            03.10.2017 10:20
            +1

            Паскале — нет, мнемоники никакой нет.

            Почему нет? Вроде, есть. Не везде допустима, но есть.

            var
            abc: ^Integer;
            ...
            abc^ := 123;


            1. MacIn
              03.10.2017 14:24
              +1

              Вы не поняли.
              В сях будет так:
              int *a
              Т.е. «если дереференснуть а, будет int», потому что * перед — это разыменовывание. Так что такое объявление логично.
              А паскалевское такой мнемоники не имеет — каретка прыгает то перед переменной, то после.
              Если было бы ^abc = было бы так же мнемоничненько.


              1. flatscode
                03.10.2017 15:16
                -2

                Для меня то, что в паскале — логичнее. Есть переменная, есть ее тип.
                Тип с крышкой (^) впереди — это ссылочный тип. Все просто и понятно.


                1. MacIn
                  03.10.2017 17:36
                  -1

                  Вы все равно не поняли.
                  В сях есть мнемоника, которая не требует запоминания.

                  Тип с крышкой (^) впереди — это ссылочный тип. Все просто и понятно

                  Нет, каретка ставится перед тем, на что ссылку описываем, а не перед самим типом. Никакой логикой тут не пахнет.
                  PMyType = ^TMyType;
                  Ссылочный тип тут PMyType, а не TMyType. Было бы PMyType = TMyType^; было бы как в Си, логично. Но низя: требование однопроходности паскалевского компилятора. Не скажу, что я не доволен этим балансом, tradeoffом так сказать.


                  1. flatscode
                    03.10.2017 19:14
                    +1

                    В сях есть мнемоника, которая не требует запоминания.

                    Ну как не требует запоминания? Программисты на C святым духом что ли пользуются?

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

                    Ссылочный тип тут PMyType, а не TMyType.

                    Ссылочный тип здесь ^TMyType и, по счастливой случайности, PMyType.


          1. netch80
            04.10.2017 21:41

            Эта логика начинает резко хромать, когда определения усложняются:

            typedef int (*fun)(void*);
            


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

            А вот такое компиляторы уже не осиливают, выдавая ошибки:

            typedef int (*fun1)(int) (*fun2)(int);
            


            хотя, казалось бы, чего сложно — определить fun2 как указатель на функцию с int параметром и указателем на функцию int->int в результате…
            ан нет, случился зашкал сложности, и без промежуточных typedef?ов не выкрутиться.

            Стиль таких определений в Pascal, Go (*) и многих других — читаемее и не страдает такими ограничениями. Цена же за это — что надо, например, явно писать слова var — для переменных, func — для определений функций. Как по мне, цена вполне разумная, польза перевешивает.

            (*) Хоть я его и громко ругаю за кучу прочего.


  1. Amareis
    03.10.2017 05:37

    Помнится, Спольски разорялся по поводу языков, в которых нет указателей (ну и рекурсию сюда же прицепил) http://local.joelonsoftware.com/wiki/Опасности_обучения_на_Java


  1. MacIn
    03.10.2017 09:30

    дизайнеры

    Designer = разработчик, или в данном контексте «создатель» (языка программирования). Дизайнер — тот, кто создает одежду или интерьер для квартиры, такова коннотация в русском яыке.


  1. martin__marlen
    03.10.2017 10:24
    +1

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


    1. quasilyte
      03.10.2017 12:11
      -1

      Если очень захотеть, то получить адресную арифметику в Go можно.
      Это будет выглядеть примерно как в Rust, или, если позволите, «не как в Си».

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


      1. quasilyte
        03.10.2017 14:25

        `unsafe.Pointer` и `uintptr` в совокупности с возможностью преобразовывать их друг в друга дадут возможность читать по указателю со смещением.

        Это может быть полезно при работе, например, с сишными библиотеками.

        Point в том, что там, где реально нужно использовать арифметику указателей, делать это можно. Тезис «людям не нравится адресная арифметика» не обоснован,
        корректнее сказать: «большинству не нравится адресная арифметика там, где можно было обойтись без неё».

        P.S. — если кто-то этим воспользуется, стоит мониторить
        proposal: spec: disallow T<->uintptr conversion for type T unsafe.Pointer


    1. youROCK
      03.10.2017 15:21

      Это, как минимум, сильно усложняет жизнь Garbage Collector'у и не позволяет компилятору вставить за вас проверки на выходы за границы массива, например.


  1. TargetSan
    03.10.2017 11:33

    Подскажите пожалуйста одну вещь. В своё время я задавал вопрос, почему существует такая вещь как not-so-nil interface pointers — ситуация, когда nil-указатель на переменную конкретного типа неявно приводится к указателю на интерфейс, который этот тип реализует. И, как результат, у нас получается fat pointer с nil data pointer и валидным vtable pointer. Следствие — nil pointer dereference в рантайме, где его не ждали. Но вопрос не в этом. Вопрос в том, что ответ тогда был "потому что в Go указатели, а не ссылки". Как раз этот ответ я и не понял.


    1. divan0 Автор
      03.10.2017 11:36
      -1

      Я тоже этот ответ не понял. Я бы сказал, что это становится понятным, если посмотреть как интерфейсы под капотом устроены (ну, мне, по крайней мере стало понятно): habrahabr.ru/post/325468/#interfeysy


      1. TargetSan
        03.10.2017 11:44

        Мне в общем-то тоже понятно, почему так происходит. Мне даже понятно, для чего используется эта фича. Что мне, к сожалению, осталось непонятно — почему это разрешено в виде неявного преобразования. Как по мне, сильно повышает шансы наступить на грабли. Было бы в виде явного тайп каста, так что неявный приводит к fully nil interface pointer — вопросов бы не было. Впрочем, как реализовали так реализовали. Может в v2 поменяют. Или давно есть линт, о котором я не знаю.


        1. divan0 Автор
          03.10.2017 11:46
          -1

          А покажите пример, похоже я не до конца понял проблему.


          1. TargetSan
            03.10.2017 12:11

            Вырожденный пример будет какой-то такой


            // Note: both FirstError and SecondError implement 'error' interface
            // and use some data from value when their respective Error() is called
            
            // May return non-nil error, if code fails
            func first(arg MyData) *FirstError { /* some code here */ }
            // May also return non-nil error, if code fails
            func second(arg MyData) *SecondError { /* some other code here */ }
            
            func third(arg MyHugeData) error {
                if err := first(arg.DataField); err != nil {
                    return err
                }
                return second(arg.DataField)
            }
            
            func fourth(arg MyHugeData) {
                if err := third(arg); err != nil {
                    fmt.Println(err.Error())
                }
            }

            Такой код КМК вполне может случиться — "просто верни статус последней операции дальше по стеку". Во-первых, результат всегда будет трактоваться как неуспех. Во-вторых, при попытке узнать причину мы получим nil-pointer panic из по сути ниоткуда. Пример с обработкой ошибок КМК просто будет самый типичный. Я в своё время нарвался на такое поведение в другом контексте, подробностей уже не помню.
            Но суть была именно такой:


            1. Есть несколько типов, приводимых к одному интерфейсу.
            2. Каждый из типов требует не-нулевой инстанс чтобы корректно реализовывать указанный интерфейс
            3. В некоем коде, допустим при передаче через канал, указатель на объект какого-либо из конкретных типов приводится к интерфейсу. При этом для канала nil pointer является абсолютно корректным положением вещей и обрабатывается на принимающей стороне как положено.
            4. На принимающей стороне не-нулевой указатель на интерфейс неожиданно оказывается очень даже нулевым, т.к. сам по себе vtable бесполезен. Результат — NPE где не ждали.


            1. divan0 Автор
              03.10.2017 12:16
              -2

              Ну да, так запутанно, конечно.
              Ошибка тут, конечно очевидная — возврат ошибки по значению, а не с помощью интерфейса error из First и Second. Тоесть — это как бы и не проблема (с точки зрения компилятора), но это немного самодельный способ обработки ошибки — «А если first еще и другой тип ошибки может вернуть, а не только *FirstError)?».


              1. TargetSan
                03.10.2017 12:31
                +2

                Пример с обработкой ошибок просто самый наглядный. Во второй половине комментария описан чуть более сложный случай. Добавьте туда пункт 2.5:


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


                Проблема именно в том, что в сколь-нибудь нетривиальном коде вполне можно получить "битый интерфейс" на ровном месте.


  1. Arbane
    03.10.2017 12:46

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


    Если нет арифметики указателей, по мне было бы логичнее использовать два ключевых слова — для типов по ссылке и для типов по значению. Скажем class и record.

    Указатель на один тип данных можно разыменовывать как на другой в Go? Если да, то вообще решение убрать арифметику будет странным.

    Если нет, то можно было бы определить типы «указателей на» и «значений» и все.

    var a int
    var b pInt
    
    pInt = class int