Конверсии в Go

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

Go - это строго типизированный язык программирования. Когда речь идет о типах, он строг, и во время компиляции будут сообщаться об ошибках:

package main
import "fmt"
func main() {
    monster := 1 + "2"
    fmt.Printf("monster: %v\n", monster)
}
> go build
# github.com/mlowicki/lab
./lab.go:6: cannot convert "2" to type int
./lab.go:6: invalid operation: 1 + "2" (mismatched types int and string)

JavaScript - один из слабо (слабо) типизированных языков. Давайте посмотрим, как это работает:

var monster = 1 + "foo" + function() {};
console.info("type:", typeof monster)
console.info("value:", monster);

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

type: string
value: 1foofunction () {}

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

func f(text string) {
    fmt.Println(text)
}
func main() {
    f(string(65))  // integer constant converted to string
}

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

f(65)

вызывает ошибку времени компиляции: «невозможно использовать 65 (тип int) в качестве строки типа в аргументе для f».

Базовый тип

Базовый тип - строка, логический, числовой или буквальный тип - это один и тот же тип. В противном случае это базовый тип из объявления типа:

type A string             // string
type B A                  // string
type C map[string]float64 // map[string]float64 (type literal)
type D C                  // map[string]float64
type E *D                 // *D

(базовые типы помещаются внутри комментариев)

Если базовые типы совпадают, тогда преобразование действительно на 100%:

package main
type A string
type B A
type C map[string]float64
type C2 map[string]float64
type D C
func main() {
    var a A = "a"
    var b B = "b"
    c := make(C)
    c2 := make(C2)
    d := make(D)
    a = A(b)
    b = B(a)
    c = C(c2)
    c = C(d)
    var _ map[string]float64 = map[string]float64(c)
}

Ничто не мешает компилировать такую ​​программу. Определение базовых типов не рекурсивно:

type S string
type T map[S]float64
...
var _ map[string]float64 = make(T)

поскольку он дает ошибку времени компиляции:

cannot use make(T) (type T) as type map[string]float64 in assignment

Это происходит потому, что базовым типом T является не map [string] float64, а map [S] float64. Конверсия тоже не будет работать:

var _ map[string]float64 = (map[string]float64)(make(T))

Вызывает ошибку при построении:

cannot convert make(T) (type T) to type map[string]float64

Возможность присвоения

В спецификациях Go используется термин возможность назначения. Он определяет, когда значение v может быть присвоено переменной типа T. Давайте посмотрим на его одно правило в действии, которое говорит об одних и тех же базовых типах, если хотя бы один тип не назван:

package main
import "fmt"
func f(n [2]int) {
    fmt.Println(n)
}
type T [2]int
func main() {
    var v T
    f(v)
}

Эта программа печатает «[0 0]». Преобразование возможно во всех случаях, разрешенных законом о переуступке. Таким образом, программист может явно отметить свое сознательное решение:

f([2]int(v))

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

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

Константы

Константу v можно преобразовать в тип T, когда v можно представить значением типа T:

a := uint32(1<<32 – 1)
//b := uint32(1 << 32) // constant 4294967296 overflows uint32
c := float32(3.4e38)
//d := float32(3.4e39) // constant 3.4e+39 overflows float32
e := string("foo")
//f := uint32(1.1) // constant 1.1 truncated to integer
g := bool(true)
//h := bool(1) // convert 1 (type int) to type bool
i := rune('ł')
j := complex128(0.0 + 1.0i)
k := string(65)

Подробное знакомство с константами доступно в официальном блоге.

Числовые типы

число с плавающей запятой → целое

var n float64 = 1.1
var m int64 = int64(n)
fmt.Println(m)

Дробная часть удаляется, поэтому код печатает «1».

Для других преобразований:

  • число с плавающей запятой → число с плавающей запятой,
  • целое → целое,
  • целое → число с плавающей запятой,
  • сложный → сложный.

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

var a int64 = 2 << 60
var b int32 = int32(a)
fmt.Println(a)         // 2305843009213693952
fmt.Println(b)         // 0
a = 2 << 30
b = int32(a)
fmt.Println(a)         // 2147483648
fmt.Println(b)         // -2147483648
b = 2 << 10
a = int64(b)
fmt.Println(a)         // 2048
fmt.Println(b)         // 2048

Указатели

Возможность присвоения работает с типами указателей так же, как и с литералами других типов:

package main
import "fmt"
type T *int64
func main() {
    var n int64 = 1
    var m int64 = 2
    var p T = &n
    var q *int64 = &m
    p = q
    fmt.Println(*p)
}

Программа работает нормально и выводит «2», полагаясь на уже рассмотренное правило назначаемости. Базовые типы * int64 и T одинаковы, и, кроме того, * int64 не имеет имени. Конверсия добавляет больше возможностей. Для безымянных типов указателей достаточно, чтобы базовые типы указателей имели один и тот же базовый тип:

package main
import "fmt"
type T int64
type U W
type W int64
func main() {
    var n T = 1
    var m U = 2
    var p *T = &n
    var q *U = &m
    p = (*T)(q)
    fmt.Println(*p)
}

* T заключен в скобки, иначе он интерпретировался бы как * (T (q)) .

Так же, как и раньше, программа печатает «2». Базовые типы U и T одинаковы, и они используются в качестве базовых типов в * U и * T. Задание:

p = q

не будет работать, потому что он имеет дело с двумя разными базовыми типами: * T и * U. В качестве упражнения давайте рассмотрим, что произойдет, если мы немного изменим объявления:

type T *int64
type U *W
type W int64
func main() {
    var n int64 = 1
    var m W = 2
    var p T = &n
    var q U = &m
    p = T(q)
    fmt.Println(*p)
}

Декларации U и W были изменены. Подумайте на мгновение, что произойдет ...

Компилятор указывает на ошибку «не удается преобразовать q (тип U) в тип T»:

p = T(q)

Это потому, что базовым типом p типа является * int64, но для типа q это * W. Тип q имеет имя (U), поэтому правило о получении базового типа базового типа указателя здесь не применяется.

Струны

целое число → строка

Передача числа N во встроенную функцию string преобразует его в строку в кодировке UTF-8, содержащую символ, представленный N.

fmt.Printf("%s\n", string(65))
fmt.Printf("%s\n", string(322))
fmt.Printf("%s\n", string(123456))
fmt.Printf("%s\n", string(-1))

выход:

A
ł
�
�

Два первых преобразования используют полностью допустимые кодовые точки. Вы можете задаться вопросом, что за странный символ отображается в последних двух строках. Это замещающий символ, который входит в блок Unicode, называемый specials. Его код \ uFFFD (подробнее).

Краткое введение в струнные

Строки - это в основном кусочки байтов:

text := "abł"
for i := 0; i < len(text); i++ {
    fmt.Println(text[i])
}

выход:

97
98
197
130

97 и 98 - это символы «a» и «b» в кодировке UTF-8. 3-я и 4-я строки представляют собой кодировку буквы «ł», которая в UTF-8 занимает два байта.

Цикл диапазона помогает перебирать кодовые точки в соответствии с определением Unicode (кодовая точка в Go называется руной):

text := "abł"
for _, s := range text {
    fmt.Printf("%q %#v\n", s, s)
}

выход:

'a' 97
'b' 98
'ł' 322

Чтобы узнать больше о глаголах форматирования, таких как% q или% # v, см. Документацию пакета fmt.

Гораздо больше информации в Строки, байты, руны и символы в Go ». Преобразование между строками и кусочками байтов или рун больше не должно быть таким странным после этого краткого объяснения.

строка ↔ кусок байтов

bytes := []byte("abł")
text := string(bytes)
fmt.Printf("%#v\n", bytes) // []byte{0x61, 0x62, 0xc5, 0x82}
fmt.Printf("%#v\n", text)  // "abł"

Срез содержит байты преобразованной строки UTF-8.

нить ↔ кусочек рун

runes := []rune("abł")
fmt.Printf("%#v\n", runes)         // []int32{97, 98, 322}
fmt.Printf("%+q\n", runes)         // ['a' 'b' '\u0142']
fmt.Printf("%#v\n", string(runes)) // "abł"

Срез, созданный из преобразованной строки, содержит кодовые точки Unicode (руны).

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

Ресурсы