写在开头
非原创,知识搬运工,本节介绍了基本数据类型及长度,字符串、byte和rune之间的区别,如何比较字符串
demo代码地址
目录
带着问题去阅读
- len函数和unsafe.Sizeof的使用场景区别
- 整型溢出会发生什么现象?
- 无符号和有符号的区别,以及int64和uint64的取值范围多少
- 类型别名如何定义
- 分别说明GO中单引号、双引号以及尖引号代表的含义
- 码点的含义和与rune类型有什么关系
- rune类型和byte的区别
- "go语言"这个字符串在len/unsafe.Sizeof函数中输出的结果是多少?[]rune("go语言")和[]byte("go语言")的成员数量又有多少
- 字符串的比较是根据什么
知识点前瞻
前置知识(字、字长、字节、位)
- bit字和位都表示一个二进制
- byte字节,一个字节由8个字组成,8位
- word字 ,多个字节为一个字,不同计算机的字大小不同,32位1字=32位=4字节,64位1字=64位=8字节
-
字长,字的位数叫字长,即字的长度就是字长,长度用位数表示(不固定,有可变和固定两种)一般地,大型计算机的字长为32―64位,小型计算机为16―32位,而微型计算机为4一16位。
计算机的字长决定了其CPU一次操作处理实际位数的多少,由此可见计算机的字长越大,其性能越优越。
我们常说的32位64位一般是指CPU数据总线的长度,即数据大小为2^位数,字大小与位数有关
1.数字类型(Numeric types)
我们使用unsafe.Sizeof来获取变量占用的字节大小
1.1整型-平台无关整型
在任何CPU架构下或操作系统中,长度都是固定不变的(int8/16/32/64,uint8/16/32/64),类型长度大小与数字有关,即int8为8个字,一个字节
1.1.1无符号和有符号的区别
同样字节长度下,最高位(从右到左,表示低到高)比特为符号位
源码:
01111111
取反:
10000000
+1得到整型编码
10000001
不能简单的通过printf(%b)得到
摘取自官方文档
The value of an n-bit integer is n bits wide and represented using two's complement arithmetic
1.2整型-平台相关整型
先来看看res怎么描述的
uint either 32 or 64 bits
int same size as uint
uintptr an unsigned integer large enough to store the uninterpreted bits of a pointer value
uintptr可以看作是一个超级大的无符号整型(大到可以容纳任何值)
这是其底层c++中的定义,是一个无符号的长长整型
typedef unsigned long long int uint64;
typedef uint64 uintptr;
这三类是跟平台相关的,根据电脑位数大小为4字节或8字节
var a uint8 = 1
var b uint16 = 1
var c int = 1
var d int8 = 1
var e int16 = 1
var f = 1
fmt.Println(unsafe.Sizeof(a)) //1
fmt.Println(unsafe.Sizeof(b)) //2
fmt.Println(unsafe.Sizeof(c)) //8
fmt.Println(unsafe.Sizeof(d)) //1
fmt.Println(unsafe.Sizeof(e)) // 2
fmt.Println(unsafe.Sizeof(f)) //8
整型后面的数字表示占用多少个字bit,当不显示声明类型时,类型推断为int,这里因为我的系统是64位的,即int类型占用8字节Byte64字bit,取值范围为0-(2^63)-1,63是因为最高位为符号,-1是因为没有-0这个数值存在
fmt.Println(math.MaxInt) //9223372036854775807 即2^63-1
因此我们要注意移植不同系统时,这类变长类型的长度
1.3不同进制的格式化输出
打开电脑计算器发现,我们用B(BIN表示二进制)、O(OCT表示8进制)、D(DEC表示10进制),H(HEX表示16进制)
编程语言中表示类似(唯一注意的是16进制)
- 默认10进制 %d
- 0b或0B 二进制 %b
- 0o或0O 八进制 %o
- 0x 16进制 %x
func TestInt() {
var num01 int = 0b1100
var num02 int = 0o14
var num03 int = 0xC
fmt.Printf("2进制数 %b 表示的是: %d \n", num01, num01)
fmt.Printf("8进制数 %o 表示的是: %d \n", num02, num02)
fmt.Printf("16进制数 %X 表示的是: %d \n", num03, num03)
}
2进制数 1100 表示的是: 12
8进制数 14 表示的是: 12
16进制数 C 表示的是: 12
同时fmt的Printf格式化输出为下
%b 表示为二进制
%c 该值对应的unicode码值
%d 表示为十进制
%o 表示为八进制
%q 该值对应的单引号括起来的go语法字符字面值,必要时会采用安全的转义表示
%x 表示为十六进制,使用a-f
%X 表示为十六进制,使用A-F
%U 表示为Unicode格式:U+1234,等价于"U+%04X"
%E 用科学计数法表示
%f 用浮点数表示
1.4整型溢出问题
看一个demo
func TestIntOverflow(t *testing.T){
var a int8 = 127
a+=1
fmt.Println(a)
}
=== RUN TestIntOverflow
-128
--- PASS: TestIntOverflow (0.00s)
天哪,该demo结果居然是-127,即边界是127,+1后不是128,超出边界后是绕了一圈成了-128(同理uint8,255+1 = 0 )
1.4浮点型
float只提供了float32/64两种浮点类型,即4字节和8字节
func TestFloat(t *testing.T){
var a float32
var b float64
t.Log(unsafe.Sizeof(a),unsafe.Sizeof(b))
}
=== RUN TestFloat
int_test.go:43: 4 8
--- PASS: TestFloat (0.00s)
两者都是IEEE-754标准,32位是单精度,1位表示符号S,8位用来指数E,剩下23位表示尾数M。
64位双精度,1位符号,11位指数,52位尾数
对于 float32(单精度)来说,表示尾数的为23位,除去全部为0的情况以外,最小为2-23,约等于1.19*10-7,所以float小数部分只能精确到后面6位,加上小数点前的一位,即有效数字为7位。同理 float64(单精度)的尾数部分为 52位,最小为2-52,约为2.22*10-16,所以精确到小数点后15位,加上小数点前的一位,有效位数为16位。尾数越多,精确到小数位数越多
math.MaxFloat32
math.MaxFloat64
有了符号S、指数E和尾数M,那么我们表示一个小数就可以用公式
看S我们可以知道S=0时-1^0 = 1小数>=0.0
offset为阶码偏移值是经过换算的指数
IEEE-754转换过程太复杂,不了解了摆烂
我们只需注意浮点型的比较,本质是二进制比较,但是转为二进制有精度丢失,因此不准确
1.5复数类型
查阅了好多资料,发现跟数学有关,寄,跳过了(论学好高数的重要性)
1.6类型别名和比较
我们可以利用type来给一个类型取别名
但是当我们将别名和原类型做比较时,发现报错,提示类型不匹配
demo
type myint int
var a int =1
var b myint =2
t.Log(a==b) //error
go语言对类型安全有严格的要求,即便底层类型相同,但仍是不同类型的数据,不能被混在表达式中
2.字符串类型(string type)
与C不同的是GO没有字符型(char 关键字),但仍可以通过引号的区别来区分
在GO中,通过string类型统一了对“字符串”的抽象,无论是字符串常量、变量都被统一设置位string,是GO语言原生支持的(如C语言没有原生的字符串,通过字符数组以'\0'结尾表示字符串)
问题来了,为什么GO要原生支持字符串:
- 不是原生类型,编译器不会进行类型校验,类型安全差
- c中字符串操作要时刻考虑“\0”,防止缓冲区溢出
- c以字符串数组形式定义数组,值可变,并发场景需要考虑
- 不是原生获取其长度代价大,比如c中使用strlen函数,其原理是需要遍历整个字符串直到"\0"O(n)
- c语言没有内置对非ASCII字符的支持(如中文字符,输出可能变为乱码)
因此相比于C我们需要注意:
- string类型的数据是不可变的
demo
var s string = "zjb"
s[1] = g // error
s = "www"
虽然仍可以通过下标访问的方式s[1]获取数据,但是我们却不能简单的通过下标的方式来改变部分字符串,即所在程序的内存位置不能只修改部分,其实它底层不是一个数组,是一个stringstruct结构体
runtime/string.go
type stringStruct struct {
str unsafe.Pointer
len int
}
这里的len是小写,因此我们访问不到,只能通过len函数来访问
反射类的包中也有描述
reflect/value.go
type StringHeader struct {
Data uintptr
len int
}
stringHeader是一个string的运行时表示
str才是虚拟内存地址的指针,因为len的存在我们获取产长度的时间复杂度为O(1),
同时,这也意味着我们直接将string类型传参入函数也不会有太大开销(GO传参是值传递,在调用函数前,会先在栈空间开辟一块内存拷贝参数)
Strings are immutable: once created, it is impossible to change the contents of a string. The predeclared string type is
string
; it is a defined type.
代码验证一下
var a string = "yes"
var b string = "我爱你"
fmt.Println(len(a)) //3
fmt.Println(len(b)) //9
fmt.Println(unsafe.Sizeof(a)) //16
fmt.Println(unsafe.Sizeof(b)) //16
fmt.Println(unsafe.Sizeof("我")) //16
我们可以看到 sizeof输出的结果都是16即两个 int(8个字节)的大小,这确实是一个指针
2.1字符串的编码
A string value is a (possibly empty) sequence of bytes. The number of bytes is called the length of the string and is never negative.
从ref描述来看,字符串本质是一个可以为空的字符序列byte组成,len的长度实际是这个byte的长度,因此这也是为什么在GO中中文字符串的长度为什么很奇怪的原因。
在GO中,字符使用Unicode编码(通常用16进制表示),unicode每个字符都分配了统一且唯一的字符编号(类似ASCII码‘A’的65),Unicode码点是字符在编码中的位置,一个码点对应一个字符
demo
func TestStr(t *testing.T){
var a string = "中国"
t.Log(len(a))
for _,c := range a {
fmt.Printf("%x\n",c)
}
}
=== RUN TestStr
6
4e2d
56fd
--- PASS: TestStr (0.00s)
“中”在unicode中的表示是0x4e2d,即0x4e2d是“中”在Unicode字符集表中的码点
for range遍历的是一个码点
题外话--UTF8/16的诞生
ASCII码是一个字节大小(8位),取值范围是0-255,但实际ASCII码一共定义了128个字符,对于英语来说是够用的,但亚洲语言的字符数量不止255个,一个字节已经完全不够用,出于此目的诞生unicode,它实际上是一本字典,为每个字符规定一个用来表示该字符的数字。但是Unicode没有规定字符对于的二进制如何存储,如“汉”,0x6c49,110110001001001d,二进制15位,至少需要2个字节,那在“汉”之后出现在该本字典中的字符有没有可能需要3到4个存储?计算机又怎么知道到底是2个字节表示一个字符还是3个4个?,因此诞生UTF-8/16,其原理是根据码点进行编码
2.2 rune类型
A rune literal represents a rune constant, an integer value identifying a Unicode code point.
rune类型表示一个码点,它实际上是int32别名,一个rune实例就是一个Unicod字符,一个GO字符就是rune实例的合集
3.字符类型(byte)
go语言把字符分为rune和byte,rune是int32别名用于存放多字节字符,unicode码点,byte是uint8别名,用于存放1字节的ASCII字符
demo
func TestComplieByteWithRune(t *testing.T){
var a string = "go语言"
fmt.Println(len(a))
fmt.Println([]rune(a))
fmt.Println([]byte(a))
}
=== RUN TestComplieByteWithRune
8
[103 111 35821 35328]
[103 111 232 175 173 232 168 128]
--- PASS: TestComplieByteWithRune (0.00s)
byte存不了多字节所以分成多个字节,len函数计算的是byte长度(g和o分别占一个字节,中国两字占6字节,因此该切片一共8个成员)
还有就是双引号是表示字符串
单引号是表示byte或rune类型,对应uint8和int32类型,默认是rune类型
t.Log(unsafe.Sizeof('y')) //4
t.Log(unsafe.Sizeof((byte)('y'))) //1
补充一点反引号是字符串字面量,就是字面意思,不支持任何转义,你输入啥样就啥样
4 字符串比较问题
String values are comparable and ordered, lexically byte-wise
这是GO语言规范对于字符串比较的描述:
- 字符串是可比较的
- 字符串是有序的
- 是逐字节比较的
用几个例子展开说明一下
demo
func TestCompareStr1(t *testing.T) {
s1 := "12345"
s2 := "2"
t.Log(s1 > s2)
t.Log([]byte(s1), []byte(s2))
}
false
[49 50 51 52 53] [50]
实际是逐字节ASCII玛比较, 50>49,到这里已经比出大小,后续不再对比,结果为false
当s2="12"时
true
[49 50 51 52 53] [49 50]
前面都相同,后续51没人比了,s1更大,结果为true
复杂点的中文例子
func TestCompareStr2(t *testing.T) {
s1 := "零"
s2 := "一"
s3 := "二"
t.Log(s1 > s2, []byte(s1), []byte(s2))
t.Log(s3 > s2, []byte(s3), []byte(s2))
t.Log(s3 > s1, []byte(s3), []byte(s1))
}
true [233 155 182] [228 184 128]
true [228 186 140] [228 184 128]
false [228 186 140] [233 155 182]
常见的字符串比较方法还有:
- strings.Compare
- strings.EqualFold
留疑
- 字符串连接的方式和那种方式性能最好
- 字符串与rune类型的切片转换问题
参考
1.tonyBaiGO语言第一课
2.计算机基础概念字、位、字节
3.go ref
4.整型和浮点型
5.Unicode\utf8\utf16终于懂了
6.详细讲解go中的rune