hello云胜

技术与生活

0%

类型系统

数据类型是一门语言最基础的内容。对于静态语言来说,会设计多种不同的数据类型。设计不同数据类型的目的主要是为了在编译阶段根据类型确定分配不同大小的内存。

go和java一样,同属于静态语言阵营。不同于动态语言(python、ruby,javascript等)可以在运行时通过对变量赋值的分析,自动确定内存边界,并且动态语言的变量可以在运行时赋予不同的数据类型。静态语言必须通过变量声明,显式的告诉编译器变量的类型信息。

看起来稍显麻烦,但是go提供了自动分析变量类型的能力。下面你会看到。

java

java中通常将数据类型分为两类:基本数据类型(Primitive Type)和引用数据类型(Reference Type)

基本数据类型

分为8种

类型名称 关键字 占用内存 取值范围
字节型 byte 1 字节 -128~127
短整型 short 2 字节 -32768~32767
整型 int 4 字节 -2147483648~2147483647
长整型 long 8 字节 -9223372036854775808L~9223372036854775807L
单精度浮点型 float 4 字节 +/-3.4E+38F(6~7 个有效位)
双精度浮点型 double 8 字节 +/-1.8E+308 (15 个有效位)
字符型 char 2 字节 ISO 单一字符集
布尔型 boolean 1 字节 true 或 false

引用数据类型

java中没有指针类型。java的引用数据类型就是数组、类和接口

go

go语言的类型,大体上可以分成三类:基本数据类型,复合数据类型和接口类型。

基本类型

  • 一种内置布尔类型:bool
  • 11种内置整数类型:int8uint8int16uint16int32uint32int64uint64intuintuintptr
  • 两种内置浮点数类型:float32float64
  • 两种内置复数类型:complex64complex128
  • 一种内置字符串类型:string

除了这17种基本类型,Go中有两种内置类型别名(type alias):

  • byteuint8的内置别名。 我们可以将byteuint8看作是同一个类型。
  • runeint32的内置别名。 我们可以将runeint32看作是同一个类型。

可以看到,go对整数类型定义的非常细,这样方便我们选择合适的尺寸,编程出最优化内存占用的程序。

平台相关的数据类型

整数类型中intuintuintptr没有写位数,他们的尺寸依赖于具体编译器实现。称为平台相关的数据类型。 也就是说,在64位的架构上,intuint类型的值是64位的;在32位的架构上,它们是32位的。 编译器必须保证uintptr类型的值的尺寸能够存下任意一个内存地址。

所以建议,编写有移植性要求的代码时,最后不要用这种平台相关的数据类型。

浮点型

同样存在精度问题,不可以用于金额相关业务的计算。

否则坑你没商量。

字符串

和java一样,go的string类型是不可变的。

go的优化点:

  • 对多行字符串的支持。

    在java里写多行字符串非常恶心,加一堆换行转义符。go只要用反引号引起来。你写什么就是什么。所见即所得。

  • 采用unicode编码,对中文支持友好。

字符串的长度问题

字符串的长度分字节长度和字符长度。一个汉字是一个字符,在unicode编码方案用utf-8编码存储的情况下,一个汉字是三个字节。

1
2
3
4
5
func TestChinese(t *testing.T) {
var s string = "中国"
t.Log("中国的字节长度是", len(s)) // 6
t.Log("中国的字符长度是", utf8.RuneCountInString(s)) // 2
}
字符

rune这个看起来很陌生,似乎很难理解。但是其实只要类比java的char就好。

rune,在Go中,一个rune值表示一个Unicode码点。 我们可以将一个Unicode码点看作是一个Unicode字符。 但是,我们也应该知道,有些Unicode字符由多个Unicode码点组成。不过,每个英文或中文Unicode字符值含有一个Unicode码点。

可以说,一个 rune 实例 就是一个 Unicode 字符,一个 Go的 字符串可以被视为 rune 实例的集合

一个rune字面量由若干包在一对单引号中的字符组成。包在单引号中的字符序列表示一个Unicode码点值。 rune字面量形式有几个变种,其中最常用的一种变种是将一个rune值对应的Unicode字符直接包在一对单引号中。比如:

1
2
3
'a' // 一个英文字符
'π'
'众' // 一个中文字符
1
2
3
4
5
func Test_rune(t *testing.T) {
var a rune = 'a'
t.Log(a)
t.Log(a == 97) // true
}

打印的结果是97。’a’的unicode编码就是97。两者是等价的。

go中的字符使用unicode编码方案编码的,但是在存储上使用utf-8的方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func TestRune(t *testing.T) {
var c rune = '中'
t.Logf("中的unicode码点%x", c) // 4e2d

buf := make([]byte, 3)
utf8.EncodeRune(buf, c)
t.Logf("中的unicode码点的utf-8存储0x%x", buf) // 0xe4b8ad
// 一个汉字三个字节

// 对utf-8解码
r, size := utf8.DecodeRune(buf)
t.Logf("%s", string(r))
t.Log(size)
}

组合类型

  • 指针类型 - 类似C语言指针

  • 结构体类型 - 类似C语言的结构体

  • 函数类型 - 函数类型在Go中是一种一等公民类别

  • 容器类型

    包括:

    • 数组类型 - 确定长度的容器类型
    • 切片类型 - 动态容量的容器类型
    • map类型- 也常称为字典类型。在标准编译器中映射是使用哈希表实现的。
  • 通道类型 - 通道用来同步并发的协程

  • 接口类型- 接口在反射和多态中发挥着重要角色

类型推断规则

如前所述,go的基本类型,比如int,有很多不同的尺寸类型。所以一个整数100。并不能确定其究竟是什么类型。这种情况在go中就称为值的类型不确定性。(在java中就不存在这种情况)。因为这种不确定性,就需要类型推断的原则。

  • 一个字符串的默认类型是string类型。
  • 一个布尔值的默认类型是bool`类型。
  • 一个整数型的默认类型是int`类型。
  • 一个rune字符的默认类型是rune(亦即int32`)类型。
  • 一个浮点数的默认类型是float64`类型。
  • 一个复数的默认类型是预声明的complex128类型。

类型转换问题

和java一样,go中也可以进行类型转换。这又分两者情况。基本类型和组合类型。

基本类型

在java中

1
2
3
4
5
6
@Test
public void easy() {
int a = 100;
long b = a;
int c = (int)b;
}

范围小的int型可以直接默认转为long型。但是long型转为int必须显式类型转换。

但是在go中

1
2
3
4
5
func Test_Type_Change(t *testing.T) {
var a int8 = 100
var b int64 = int64(a)
var c int16 = int16(b)
}

所有的类型转换,都必须显式进行。个人感觉,go这样处理还是好的,程序员应该知道自己在干什么。java的隐式转换其实除了省的敲两下代码,没什么用。反而容易出故障。

1
2
3
4
5
6
@Test
public void easy() {
float a = 1.3f;
int b = (int) a;
int c = (int) 1.3f;
}
1
2
3
4
5
func Test_Type_Change(t *testing.T) {
var a = float32(1.2) // 合法
var b = int(a) // 合法
var c = int(1.2) // 非法,这样写不允许
}

在go和java中,进行精度的转换都是可以的,但是go中直接进行字面量的类型转换是不允许的,这样做也是没有必须要的。所以不允许是更合理的。

常量

go中声明常量用const,java是final。

1
2
3
4
5
const a int8 = 16
const b = "bbb"
const (
C, D = int16(100), int64(88)
)

很简单。看一下就行,go中这些写法都合法。

但是go中还有一些特殊的写法。

常量声明的自动补全

在声明多个常量时,可以使用省略写法。省略的声明,go编译器在编译代码时会自动寻找前面最近的完整描述进行重写。比如

1
2
3
4
5
6
7
8
const (
X float32 = 3.1416
Y // 这里必须只有一个标识符
Z // 这里必须只有一个标识符

A, B = "Go", "Java"
C, _ // 空标识符是必需有,数量必须对上
)

go编译会自动补全为

1
2
3
4
5
6
7
8
const (
X float32 = 3.1416
Y float32 = 3.1416
Z float32 = 3.1416

A, B = "Go", "Java"
C, _ = "Go", "Java"
)

下划线_ 是go中一个空的占位符。作用是承接多返回值中的一个,但是以后不会使用,只是为了对应上。

iota

这是一个go新手初次看容易懵逼的写法。

1
2
3
4
5
6
7
8
9
10
11
const (
Failed = iota - 1 // == -1 iota = 0
Unknown // == 0 iota = 1
Succeeded // == 1 iota = 2
)

const (
Readable = 1 << iota // == 1 iota = 0
Writable // == 2 iota = 1
Executable // == 4 iota = 2
)

iota是go内置的一个常量,每次const声明时初始值是0。然后使用自动补全语法时,每一次声明,iota加1。

1
2
const a = iota  // iota = 0
const b = iota // iota = 0

所以利用iota和自动补全,可以方便的进行多个常量的初始化。

比如go内置的log包,预定义log打印格式时,就使用iota进行。

1
2
3
4
5
6
7
8
9
10
const (
Ldate = 1 << iota // the date in the local time zone: 2009/01/23
Ltime // the time in the local time zone: 01:23:23
Lmicroseconds // microsecond resolution: 01:23:23.123123. assumes Ltime.
Llongfile // full file name and line number: /a/b/c/d.go:23
Lshortfile // final file name element and line number: d.go:23.
LUTC // if Ldate or Ltime is set, use UTC rather than the local time zone
Lmsgprefix // move the "prefix" from the beginning of the line to before
LstdFlags = Ldate | Ltime // initial values for the standard logger
)

相当于

1
2
3
4
5
6
7
8
9
10
const (
Ldate = 1
Ltime = 2
Lmicroseconds = 4
Llongfile = 8
Lshortfile = 16
LUTC = 32
Lmsgprefix = 64
LstdFlags = 3
)

使用时,如果我希望打印的日志前面加上,日期 时间 文件名。那么就传1 + 2 + 8 = 11

1
log.SetFlags(11)

打印出的日志类似这样

1
2
3
2021/12/02 15:08:17 d:/github/go_learn/src/concurrent/goroutine/main/main.go:11: hello!
2021/12/02 15:08:17 d:/github/go_learn/src/concurrent/goroutine/main/main.go:11: hi!
2021/12/02 15:08:19 d:/github/go_learn/src/concurrent/goroutine/main/main.go:11: hi!

变量声明

go在变量声明的语法上比java灵活很多。

首先,一个最标准的变量声明如下

1
var a int = 10

一个明显的区别:go把变量名放在了变量类型的前面。

为什么?Go’s Declaration Syntax - Go 语言博客 (go-zh.org)

官方有个解释,大体是说,和C相比,在一些复杂函数,参数时指针的情况下,将类型放在后面读起来更容易理解。

自动类型推断

go可以省略类型信息进行变量声明

1
var b = 10

根据前面说的类型推断规则,b会被推断为int型。

除了之前说的类型推断之外,还支持一次声明多个变量

1
var a, b = "Go", false  //可以一次声明多个不同类型的变量

但是

1
var a string, b bool = "Go", false  //语法错误。确定类型的声明,一次多个变量只能是一种类型

还可以使用()一次声明多个

1
2
3
4
var (
a string = "GO"
b bool = false
)

短声明语法

然后,重要的来了。go的短声明语法。

1
a, b := "Go", false

省略了var。

和var声明的一个重要区别是:短声明语法只能用在方法内。

变量的作用域

go和java一样(以及其他一切高级语言),变量都有不同的作用域。

java可以通过private,public等关键字设置变量的作用域。

go没有这些关键字,通过另外一种规范确定作用域。一般而言,可以把go中变量按作用域分成两类:包级变量和局部变量。

image-20211224150846337

可以感觉到go的变量声明很灵活。但是有点重复了。这与go崇尚的简单原则有些冲突。go的作者之一rob pike也曾表示过如果重新来设计一次变量声明语法,大概率会砍掉一些灵活性,保持统一性。

变量遮蔽

go和java一样。没什么好说的。基本上语言都是这样。同名变量,局部变量遮蔽包级变量。等等。

变量遮蔽是我们写代码经常犯的错误,但是很低级。

类型别名

在上面的基本类型中,提到了byteuint8的内置别名,runeint32的内置别名。

类型别名这个说法在java中是不存在的。

go的这个类型别名究竟是怎么回事?

go源码中是这样定义byte和rune的

1
2
3
4
5
6
7
8
// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
// used, by convention, to distinguish byte values from 8-bit unsigned
// integer values.
type byte = uint8

// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32

byte和unit8,rune和int32是完全一样的。

类型别名和类型定义语法上的区别就是多了个=

1
2
3
4
5
var a rune 
var b int32 = 132
a = b //可以直接赋值
var c int = 132
// t.Log(a == c) 类型不匹配,不可以直接比较

类型定义

这里要注意类型别名和类型定义的语法。关键字type用来定义类型

1
2
3
4
5
type Age int
type (
Name string
Sex int8
)

这是类型定义的语法。定义一种新的类型,没有=

  • 类型定义的新类型和原类型是两种不同的类型。虽然他们的底层类型都是int
  • 两个底层类型相同的新类型,相互之间转换需要显式执行。而类型别名的数据可直接转换。
  • 类型定义可以写在func里,作用域也只在这个func里