Как представить необязательную строку в Go?

Я хочу смоделировать значение, которое может иметь две возможные формы: отсутствует или строка.

Естественный способ сделать это — использовать Maybe String, или Optional<String>, или string option и т. д. Однако в Go нет таких типов вариантов.

Затем я подумал, следуя за Java, C и т. д., что альтернативой будет обнуляемость или nil в Go. Однако nil не является членом типа string в Go.

Поискав, я подумал использовать тип *string. Это могло бы сработать, но кажется очень неудобным (например, я не могу взять адрес строкового литерала так же, как я могу взять адрес структурного литерала).

Каков идиоматический способ моделирования такого значения в Go?


person jameshfisher    schedule 09.06.2015    source источник
comment
Связано: stackoverflow.com/a/30716481/1705598. Это для *int64, но вы можете использовать те же решения для *string.   -  person icza    schedule 09.06.2015
comment
Есть ли в вашем приложении семантическая разница между неустановленной (отсутствующей) строкой и ? По моему опыту, в большинстве случаев вы можете эффективно относиться к ним одинаково. Пустая строка "" — это нулевое значение для строки в go. Это идиоматично, но, может быть, вам нужно это различие?   -  person SkyeC    schedule 09.06.2015
comment
@SkyeC проблема в том, что в типах не указывается необязательность; это просто ненадежная условность.   -  person jameshfisher    schedule 15.06.2015


Ответы (3)


Вы можете использовать что-то вроде sql.NullString, но лично я предпочитаю *string. Что касается неловкости, это правда, что вы не можете просто sp := &"foo" к сожалению. Но для этого есть обходной путь:

func strPtr(s string) *string {
    return &s
}

Вызовы strPtr("foo") должны быть встроены, так что это фактически &"foo".

Другая возможность - использовать new:

sp := new(string)
*sp = "foo"
person Ainar-G    schedule 09.06.2015
comment
Спасибо. Кажется, что решение с указателем так же хорошо, как и я, что разочаровывает. Почему, ну почему в 21 веке в языках до сих пор нет вариантов типов/суммы/АТД. - person jameshfisher; 15.06.2015
comment
Тип Optional<String> добавляет значение в языки, которые используют исключения для обработки ошибок. Однако Golang делает обработку ошибок явной через возвращаемые значения. - person Michal Čizmazia; 15.02.2019
comment
Языки @MichalČizmazia, такие как Haskell, используют Maybe (то есть Optional под другим именем) и при нормальных обстоятельствах не используют исключения для обработки ошибок. На самом деле типы Optional не имеют ничего общего с исключениями и являются проверкой времени компиляции (в отличие от исключений). - person Andres F.; 02.01.2020
comment
@АндресФ. Ты прав! Функциональное программирование Optional выходит далеко за рамки getOrThrow операции, о которой я говорил. - person Michal Čizmazia; 04.01.2020
comment
@jameshfisher Я рекомендую использовать sql.NullString, указатель строки может быть ужасной вещью, которую вам нужно проверять на nill и уважать его везде, и это может привести к неожиданному сбою nil. - person inix; 10.03.2020

Логичным решением было бы использовать *string, как упоминал Айнар-Г. Этот другой ответ подробно описывает возможности получения указателя на значение (int64, но то же самое работает и для string). Обертка - еще одно решение.

Используя только string

Необязательный string означает string плюс 1 конкретное значение (или состояние), говорящее «не строка» (а null).

Это 1 конкретное значение может быть сохранено (сигнализировано) в другой переменной (например, bool), и вы можете упаковать string и bool в struct, и мы доберемся до оболочки, но это не вписывается в случай «использования только string" (но все еще является жизнеспособным решением).

Если мы хотим придерживаться только string, мы можем взять 1 конкретное значение из возможных значений типа string (который имеет «бесконечность» возможных значений, поскольку длина не ограничена (или, может быть, это так, как должно быть int). но это ничего)), и мы можем назвать это конкретное значение значением null, значением, которое означает "не строка".

Наиболее удобным значением для указания null является нулевое значение string, то есть пустое string: "". Назначение этого элемента null имеет то удобство, что всякий раз, когда вы создаете переменную string без явного указания начального значения, она будет инициализирована с "". Также при запросе элемента из map, значение которого равно string, также будет получено "", если ключ не находится в map.

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

Конечно, могут быть случаи, когда пустое string действительно представляет допустимое значение переменной типа string. Для этих вариантов использования мы можем выбрать другое значение.

В Go string фактически представляет собой срез байтов, доступный только для чтения. См. сообщение в блоге Строки, байты, руны и символы в Go, где это подробно объясняется.

Таким образом, string — это байтовый фрагмент, который представляет собой байты в кодировке UTF-8 в случае действительного текста. Предполагая, что вы хотите сохранить действительный текст в своем необязательном string (если вы этого не сделаете, вы можете просто использовать вместо него []byte, который может иметь значение nil), вы можете выбрать значение string, которое представляет недопустимую последовательность байтов UTF-8. и, таким образом, вам даже не придется идти на компромисс, чтобы исключить действительный текст из возможных значений. Самая короткая недопустимая последовательность байтов UTF-8 составляет всего 1 байт, например 0xff (есть и другие). Примечание: вы можете использовать функцию utf8.ValidString(), чтобы узнать, является ли значение string допустимым текстом. (допустимая последовательность байтов в кодировке UTF-8).

Вы можете сделать это исключительное значение const:

const Null = "\xff"

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

Попробуйте его на Go Playground.

const Null = "\xff"

func main() {
    fmt.Println(utf8.ValidString(Null)) // false

    s := Null
    fmt.Println([]byte(s)) // [255]
    fmt.Println(s == Null) // true
    s = "notnull"
    fmt.Println(s == Null) // false
}
person icza    schedule 09.06.2015
comment
Однако, используя этот метод, мы можем потерять контекст, в котором s может быть нулевым. - person hqt; 15.03.2020

С интерфейсным типом вы можете использовать более естественный синтаксис присваивания.

var myString interface{} // used as type <string>
myString = nil // nil is the default -- and indicates 'empty'
myString = "a value"

При ссылке на значение обычно требуется утверждение типа, чтобы сделать проверку явной.

// checked type assertion
if s, exists := myString.(string); exists {
    useString(s)
}

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

fmt.Println("myString:",myString) // prints the value (or "<nil>")

Предупреждение

При присвоении значения проверка типов не выполняется.

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

Вот демонстрация назначения с использованием интерфейсов:

var a interface{} = "hello"
var b = a // b is an interface too
b = 123 // assign a different type

fmt.Printf("a: (%T) %v\n", a, a)
fmt.Printf("b: (%T) %v\n", b, b)

Выход:

a: (string) hello
b: (int) 123

Обратите внимание, что интерфейсы назначаются путем дублирования, поэтому a и b различны.

person Brent Bradburn    schedule 14.12.2020