长期素食导致的 - [pixiv111124804]
睦头人 (\(\mathrm{a\color{red}{ctypedef}}\) 诱捕器) - [pixiv110897467]
但是这其实是一篇正经的 Golang 上手简记,并不是 MyGO 简评(MyGO 简评还在咕着(大概率不补了
鉴于后端用 go,有必要开展 golang 大学习
references: Go语言圣经、Go快速入门、《The Way to Go》中文译本、Go语言高级编程、Go语言简明教程、Go语言高性能编程
这里只以最简约的方式记录一些要点,和一点自己实验得到的认识,意在快速了解 go 的基本语言特性
参考资料主要是 references 第一个《Go 语言圣经》。引用块里的内容大多是其中的原文
0 Get Start
代码扩展名 .go
用 go run test.go
直接运行。也可以先 go build test.go
编译成可执行文件,再 ./test
用 go run <file>
来 run
单个文件,用 go run <directory>
来运行某目录下的 package main
go 项目由包 (package) 组织
任何 go 代码都以 package xxx
开始,声明此代码属于什么包
下面几行语句导入依赖包,import xxx
等
注释语法同 c/c++
注意 go 的神秘特性:
- 不允许出现被
import
但没有被使用的包 - 不允许出现声明了但没有被使用的局部名字
- go 的编译输出只有错误没有警告
非常逆天。
1 声明相关
1.0 命名
标识符命名:典规则,但支持 unicode
在 go 中,内建语句属于关键字,但一些内建类型名称,字面量和函数名属于预定义名字
预定义名字有:
内建常量: true false iota nil
内建类型: int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64 complex128 complex64
bool byte rune string error
内建函数: make len cap new append copy close delete
complex real imag
panic recover
用户标识符不能与关键字相同,但是可以与预定义名字相同,你可以在使用中重新定义他们,在一些特殊的场景中重新定义它们也是有意义的
比如可以这样
var int int
fmt.Println(int) // 0
不过你这么干之后下面就都不能再用 int
了
var a int = 1 // 报错
在全局定义的名字是“包级名字”,如全局量、全局函数等
包级名字首字母大小写决定了该名字是否在其他包中可见。小写字母开头的名字是该包私有的,可以在该包的每个源文件中访问,但其他包不能访问;大写字母开头名字是该包导出的,可以被其他包访问,比如 package fmt
中的 Printf
1.1 变量
声明语句定义了各种实体对象
var
,const
,type
,func
声明分别对应变量、常量、类型、函数
type
类似 typedef xxx xxx
或 using xxx = xxx
/*1*/ var s string = ""
/*2*/ var s string
/*3*/ var s = ""
/*4*/ var a, b, c int = 1, 2, 3
/*5*/ var a, b, c = 1, "2", 3.0
/*6*/ s := ""
/*7*/ a, b, c := 1, "2", 3.0
/*8*/ var (
a = 5
b string = ""
)
-
定义
s
,类型string
,初值为""
-
未指定初值时根据默认零值赋为
""
- 数值类型的默认零值是
0
bool
零值为false
string
零值为""
- 接口或引用类型(包括 slice、指针、map、chan 和函数)变量对应的零值是
nil
- 数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值
因此,go 中不存在未初始化的变量
-
定义
s
,类型由字面量""
推导 -
多重声明,类型均为
int
-
多重声明,类型自动推导。注意,(4) 写法仅能定义相同类型的一组量
-
简短变量声明,类型自动推导
-
多重简短声明,类型自动推导
这里有一个细节:
- 简短声明不允许用于声明全局变量
- 简短声明的
:=
左侧的若干量可以不是新定义的,如果某个量在相同的词法域已经被声明过,那么:=
对他的作用为赋值。同时要想使用:=
,左侧必须至少一个变量是新定义的
全局变量初始化在 main()
执行前完成,局部变量在执行到时初始化
1.2 常量
const x int = 1
const x = 1
const x, y, z int = 1, 2, 3
- 常量必须赋初值,不存在默认零值规则
- 常量没有简短声明
多行常量定义时,如果一个常量没有写初始化,那么抄写上一个常量的初值表达式后计算其初值
const (
a = 5
b // 5
c // 5
d = 2
)
如果多个常量写在一行定义,其多重初始化的表达式也会复制给下一行
const (
a, b = 3, 4
c, d // 3, 4
)
iota
是一种只有常量初始化能用的东西
iota
的值在每次遇到 const
的第一行初始化时变成 0。在多重声明时,每遇到下一行初始化,值就 += 1。在同一行内,iota
值相等
const a = iota // 0
const b, c = iota, iota // 0, 0
const a,
b, c = iota, iota, iota // 0, 0, 0 -> 字面上换行不影响逻辑上是一行声明的
const (
x = iota // 0
y = 10
z = iota // 2
a, b, c = iota, iota, iota // 3, 3, 3
)
const (
x = 1 + iota // 1
y // 2
z // 3
)
1.3 指针
指向 int
的指针类型为 *int
,使用 p = &x
取地址,*p
取值
右值不能被取地址。指针的零值为 nil
1.4 new
var p *int = new(int) // *p == 0
1.5 生命周期
包级名字的生命周期与程序相同
局部变量的生命周期持续到该变量不再被引用为止
那么 Go 语言的自动垃圾收集器是如何知道一个变量是何时可以被回收的呢?这里我们可以避开完整的技术细节,基本的实现思路是,从每个包级的变量和每个当前运行函数的每一个局部变量开始,通过指针或引用的访问路径遍历,是否可以找到该变量。如果不存在这样的访问路径,那么说明该变量是不可达的,也就是说它是否存在并不会影响程序后续的计算结果。
因为一个变量的有效周期只取决于是否可达,因此一个循环迭代内部的局部变量的生命周期可能超出其局部作用域。同时,局部变量可能在函数返回之后依然存在。
编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,但可能令人惊讶的是,这个选择并不是由用 var 还是 new 声明变量的方式决定的。
var global *int func f() { var x int x = 1 global = &x } func g() { y := new(int) *y = 1 }
f 函数里的 x 变量必须在堆上分配,因为它在函数退出后依然可以通过包一级的 global 变量找到,虽然它是在函数内部定义的;用 Go 语言的术语说,这个 x 局部变量从函数 f 中逃逸了。相反,当 g 函数返回时,变量
*y
将是不可达的,也就是说可以马上被回收的。因此,*y
并没有从函数 g 中逃逸,编译器可以选择在栈上分配*y
的存储空间(译注:也可以选择在堆上分配,然后由 Go 语言的 GC 回收这个变量的内存空间),虽然这里用的是 new 方式。其实在任何时候,你并不需为了编写正确的代码而要考虑变量的逃逸行为,要记住的是,逃逸的变量需要额外分配内存,同时对性能的优化可能会产生细微的影响。Go 语言的自动垃圾收集器对编写正确的代码是一个巨大的帮助,但也并不是说你完全不用考虑内存了。你虽然不需要显式地分配和释放内存,但是要编写高效的程序你依然需要了解变量的生命周期。例如,如果将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收(从而可能影响程序的性能)。
1.6 类型
type 类型名字 底层类型
与 typedef
同。这个类型名字也是和普通变量同级的名字,如果新创建的类型名字的首字符大写,则在包外部也可以使用
1.7 作用域
由于垃圾回收机制,变量的作用域不等于变量的生命周期
其他与 c++ 同
1.x 注
-
我们失去了通用的 "引用类型",现在这里只有指针了
-
关于 “元组赋值”:
f, err = os.Open("foo.txt") // ok f, err, a = os.Open("foo.txt"), true // 错误的
2 运算符の噩耗
下面是算术运算、逻辑运算和比较运算的二元运算符(按优先级排序)
* / % << >> & &^
+ - | ^
== != < <= > >=
&&
||
-
各种赋值不再是运算符。比如
=
,+=
,等等。所以a += (b += 2) // 错误的 a = b = c // 错误的
-
自增自减不再是运算符。现在
++
和--
只能后置,如a++
,并且是语句而非运算符a = b++ // 错误的
-
逗号不再是运算符。
,
被用于 “元组赋值” 去了a = 5, b = 3, c = 4 // 错误的 a = 5; b = 3; c = 4 // ok
原先能一行写完的精妙代码现在只能拆成一堆 shit
你吗的,不让压行 /fn
其他一些细节:
- 不再有
~
号。^
号在用于双目运算时表示异或,单目运算时表示按位取反 - 取模运算得到结果的符号只取决于被模数。
-5 % 2
,-5 % -2
都等价于-(5 % 2)
- 添加了一个
&^
号,表示 “位清除(bit clear)”。a &^ b
的意思是,如果b
中某一位是1
,则将a
的这一位设置为0
。注意到它等价于a & (^b)
,但是在一些情况下使用&^
要比拆开写更易于规避整形溢出
还有很重要的一点,go 要求运算符两侧操作数类型相同是非常严苛的,不会自动隐式类型转换
int
和 int16
相加会报错,int
和 float32
相加也会报错。必须显式指明类型转换才能运行
使用 int(...)
,float64(...)
来实现强制类型转换
给类型添加括号:(int)(...)
,(float64)(...)
亦可
并且:相同底层类型的不同名字被视为不同的类型,他们之间做运算也需要类型转换
type Celsius float64 // 摄氏温度
type Fahrenheit float64 // 华氏温度
Celsius 和 Fahrenheit 分别对应不同的温度单位。它们虽然有着相同的底层类型 float64,但是它们是不同的数据类型,因此它们不可以被相互比较或混在一个表达式运算。刻意区分类型,可以避免一些像无意中使用不同单位的温度混合计算导致的错误;因此需要一个类似 Celsius(t) 或 Fahrenheit(t) 形式的显式转型操作才能将 float64 转为对应的类型。Celsius(t) 和 Fahrenheit(t) 是类型转换操作,它们并不是函数调用。类型转换不会改变值本身,但是会使它们的语义发生变化。
对于每一个类型 T,都有一个对应的类型转换操作 T(x),用于将 x 转为 T 类型(译注:如果 T 是指针类型,可能会需要用小括弧包装 T,比如 (*int)(0)`)。
3 基本数据类型
3.1 整型 & 字符
int
,int8
,int16
,int32
,int64
uint
,uint8
,uint16
,uint32
,uint64
未指定大小的 int
和 uint
的具体大小由实现定义
go 中区分 “字符” 与 “字符串”,字符用单引号,字符串双引号
但是,go 没有提供字符的底层类型,你可以用各种整型来存一个字符
一般我们存 ascii 字符用 byte
,底层类型为 uint8
unicode 占用 2 或 4 字节,一般用 int16
或者 int
go 预定义了一种 unicode 类型 rune
,其底层为 int32
最后,还有一种无符号的整数类型
uintptr
,没有指定具体的 bit 大小但是足以容纳指针。uintptr
类型只有在底层编程时才需要,特别是 Go 语言和 C 语言函数库或操作系统接口相交互的地方。
- 按照通用规则,有符号整数的最高一个 bit 是符号位
- 注意,浮点数到整数的类型转换会丢掉所有小数位(向零取整)而不是四舍五入
- 以
0
开头的整数字面量将被视为八进制数,比如0666
(438)
3.2 浮点
float32
(float)大约精确到小数点后 6 位
float64
(double)大约精确到小数点后 15 位
合法的浮点数:.1
,15.
,2e5
,等等
浮点数值包括 +Inf
,-Inf
和 NaN
用 math.NaN()
获得一个 NaN
。NaN
和任何东西都不相等,包括自己。使用 math.isNaN()
来测试一个浮点数是不是一个 NaN
3.3 复数
complex64
和 complex128
,精度分别与 float32
和 float64
相同
使用 real()
和 imag
得到实部与虚部
var x complex128 = complex(1, 2) // 1+2i
var y complex128 = complex(3, 4) // 3+4i
fmt.Println(x*y) // "(-5+10i)"
fmt.Println(real(x*y)) // "-5"
fmt.Println(imag(x*y)) // "10"
复数字面量形如 2i
,1 + 2i
计算 Math.Sqrt(-1)
会得到 NaN
,用 math/cmplx
包中的 cmplx.Sqrt(-1)
得到 i
3.4 布尔
true
和 false
- go 中
and
,or
等不是运算符 - go 的
&&
和||
依旧做短路运算 - 布尔和整型互相之间均不能直接强制类型转换。逆天
3.5 字符串
字符串的类型就是 string
,不同于数组、切片等,是独立的基本数据类型
go 认为,字符串是一排不可变字节排成的序列
所以一个字符串中可以混杂着以不同的字节数存储的字符,比如:
使用 len()
获取字符串的字节数
s := "hello, world"
fmt.Println(len(s)) // 12 --> 每个字符用 1 字节
n := "mihoyo: 原神怎么你了"
fmt.Println(len(n)) // 26 --> "mihoyo: " 这一段每个字符一个字节, 后面每个汉字 3 字节
使用 s[i]
取出字符串的第 i
字节,i
必须在 0 ~ len(s) - 1
之间,否则程序 panic
使用 s[i:j]
左闭右开地取第 i ~ j
字节。string
的切片还是 string
和 python 一样,参数可以省略。比如 s[i:]
,s[:j]
,s[:]
字符串可以 +
,可以 ==
和大小比较。大小比较为逐字节比较
字符串每一字节的值不可变!不能给 s[i]
赋值
3.5.1 【关于 ASCII】
双引号包含的字符串字面量中支持各种 ascii 码转义
\a 响铃
\b 退格
\f 换页
\n 换行
\r 回车
\t 制表符
\v 垂直制表符
\' 单引号(只用在 '\'' 形式的rune符号面值中)
\" 双引号(只用在 "..." 形式的字符串面值中)
\\ 反斜杠
也可以通过 16 进制或 8 进制转义来表示一字节 ascii 字符
使用 \x
加两位 16 进制数字,比如 "A"
等同于 "\x41"
使用 \
加三位 8 进制数字,比如 "\123"
,但是字符不能超过 \377
,即 10 进制下 255
一个原生的字符串面值形式是`...`,使用反引号代替双引号。在原生的字符串面值中,没有转义操作;全部的内容都是字面的意思,包含退格和换行,因此一个程序中的原生字符串面值可能跨越多行(译注:在原生字符串面值内部是无法直接写 ` 字符的,可以用八进制或十六进制转义或 + " ` " 连接字符串常量完成)。唯一的特殊处理是会删除回车以保证在所有平台上的值都是一样的,包括那些把回车也放入文本文件的系统(译注:Windows系统会把回车和换行一起放入文本文件中)。
原生字符串面值用于编写正则表达式会很方便,因为正则表达式往往会包含很多反斜杠。原生字符串面值同时被广泛应用于 HTML 模板、JSON 面值、命令行提示信息以及那些需要扩展到多行的场景。
const GoUsage = `Go is a tool for managing Go source code. Usage: go command [arguments] ...`
3.5.2 【关于 Unicode】
通用的表示一个 Unicode 码点的数据类型是 int32,也就是 Go 语言中 rune 对应的类型;它的同义词 rune 符文正是这个意思。
我们可以将一个符文序列表示为一个 int32 序列。这种编码方式叫 UTF-32 或 UCS-4,每个 Unicode 码点都使用同样大小的 32bit 来表示。这种方式比较简单统一,但是它会浪费很多存储空间,因为大多数计算机可读的文本是 ASCII 字符,本来每个 ASCII 字符只需要 8bit 或 1 字节就能表示。而且即使是常用的字符也远少于 65536 个,也就是说用 16bit 编码方式就能表达常用字符。但是,还有其它更好的编码方法吗?
【UTF-8】
UTF8 是一个将 Unicode 码点编码为字节序列的变长编码。
UTF8 编码使用 1 到 4 个字节来表示每个 Unicode 码点,ASCII 部分字符只使用 1 个字节,常用字符部分使用 2 或 3 个字节表示。每个符号编码后第一个字节的高端 bit 位用于表示编码总共有多少个字节。如果第一个字节的高端 bit 为 0,则表示对应 7bit 的 ASCII 字符,ASCII 字符每个字符依然是一个字节,和传统的 ASCII 编码兼容。如果第一个字节的高端bit是110,则说明需要 2 个字节;后续的每个高端 bit 都以 10 开头。更大的Unicode码点也是采用类似的策略处理。
0xxxxxxx runes 0-127 (ASCII)
110xxxxx 10xxxxxx 128-2047 (values <128 unused)
1110xxxx 10xxxxxx 10xxxxxx 2048-65535 (values <2048 unused)
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 65536-0x10ffff (other values unused)
变长的编码无法直接通过索引来访问第 n 个字符,但是 UTF8 编码获得了很多额外的优点。
【字符串的 UTF8】
有很多 Unicode 字符很难直接从键盘输入,并且还有很多字符有着相似的结构;有一些甚至是不可见的字符(译注:中文和日文就有很多相似但不同的字)。Go 语言字符串面值中的 Unicode 转义字符让我们可以通过 Unicode 码点输入特殊的字符。有两种形式:
\uhhhh
对应 16bit 的码点值,\Uhhhhhhhh
对应 32bit 的码点值,其中 h 是一个十六进制数字;一般很少需要使用 32bit 的形式。每一个对应码点的 UTF8 编码。例如:下面的字母串面值都表示相同的值:
"世界"
"\xe4\xb8\x96\xe7\x95\x8c"
"\u4e16\u754c"
"\U00004e16\U0000754c"
上面三个转义序列都为第一个字符串提供替代写法,但是它们的值都是相同的。
【字符中的 UTF8】
Unicode 转义也可以使用在 rune 字符中。下面三个字符是等价的:
'世' '\u4e16' '\U00004e16'
但是,字符和字符串中的转义限制有所差别
对于小于 256 的码点值可以写在一个十六进制转义字节中,例如
\x41
对应字符 'A',但是对于更大的码点则必须使用\u
或\U
转义形式。因此,\xe4\xb8\x96
并不是一个合法的 rune 字符,虽然这三个字节对应一个有效的 UTF8 编码的码点。
go 提供的一些基本字符串工具假设你不关心一个一个的字符,只把字符串看成是字节流,比如 len()
返回的是字节数
如果关心具体每个字符,我们可以使用 unicode/utf8
包提供的工具
3.5.3 range 的隐式转换
幸运的是,go 语言的 range 循环在作用于字符串的时候,会隐式解码 utf-8 字符,例如:
3.5.4 rune 序列 & byte 序列
string
类型本身只读,但这并不意味着需要在 string
上困难的操作
将 string
对象类型转换为整型切片,比如 []rune
或者 []byte
时
s := "abc"
a := []rune(s)
b := []byte(s)
会发生这样的事:
string
对象解码 utf-8,变成 byte 序列或者每个 unicode 字符一个 rune 的 rune 序列。- 创建新的一个底层数组来存放这些整型数据
- 将切片引用自这个底层数组
将 []rune 类型转换应用到 UTF8 编码的字符串,将返回字符串编码的 Unicode 码点序列:
// "program" in Japanese katakana s := "プログラム" fmt.Printf("% x\n", s) // "e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0" r := []rune(s) fmt.Printf("%x\n", r) // "[30d7 30ed 30b0 30e9 30e0]"
(在第一个 Printf 中的
% x
参数用于在每个十六进制数字前插入一个空格。)如果是将一个 []rune 类型的 Unicode 字符 slice 或数组转为 string,则对它们进行 UTF8 编码:
fmt.Println(string(r)) // "プログラム"
将一个整数转型为字符串意思是生成以只包含对应 Unicode 码点字符的 UTF8 字符串:
fmt.Println(string(65)) // "A", not "65" fmt.Println(string(0x4eac)) // "京"
如果对应码点的字符是无效的,则用
\uFFFD
无效字符作为替换:fmt.Println(string(1234567)) // "?"
标准库中有四个包对字符串处理尤为重要:bytes、strings、strconv 和 unicode 包。strings 包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。
bytes 包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的 []byte 类型。因为字符串是只读的,因此逐步构建字符串会导致很多分配和复制。在这种情况下,使用 bytes.Buffer 类型将会更有效
关于各种包的使用略
3.6 无类型常量
Go 语言的常量有个不同寻常之处。虽然一个常量可以有任意一个确定的基础类型,例如 int 或 float64,或者是类似 time.Duration 这样命名的基础类型,但是许多常量并没有一个明确的基础类型。编译器为这些没有明确基础类型的数字常量提供比基础类型更高精度的算术运算;你可以认为至少有 256bit 的运算精度。这里有六种未明确类型的常量类型,分别是无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串。
通过延迟明确常量的具体类型,无类型的常量不仅可以提供更高的运算精度,而且可以直接用于更多的表达式而不需要显式的类型转换。例如,例子中的 ZiB 和 YiB 的值已经超出任何 Go 语言中整数类型能表达的范围,但是它们依然是合法的常量
另一个例子,math.Pi 无类型的浮点数常量,可以直接用于任意需要浮点数或复数的地方:
var x float32 = math.Pi var y float64 = math.Pi var z complex128 = math.Pi
如果 math.Pi 被确定为特定类型,比如 float64,那么结果精度可能会不一样,同时对于需要 float32 或 complex128 类型值的地方则会强制需要一个明确的类型转换:
const Pi64 float64 = math.Pi var x float32 = float32(Pi64) var y float64 = Pi64 var z complex128 = complex128(Pi64)
对于常量面值,不同的写法可能会对应不同的类型。例如 0、0.0、0i 和
\u0000
虽然有着相同的常量值,但是它们分别对应无类型的整数、无类型的浮点数、无类型的复数和无类型的字符等不同的常量类型。同样,true 和 false 也是无类型的布尔类型,字符串面值常量是无类型的字符串类型。const ( deadbeef = 0xdeadbeef // untyped int with value 3735928559 a = uint32(deadbeef) // uint32 with value 3735928559 b = float32(deadbeef) // float32 with value 3735928576 (rounded up) c = float64(deadbeef) // float64 with value 3735928559 (exact) d = int32(deadbeef) // compile error: constant overflows int32 e = float64(1e309) // compile error: constant overflows float64 f = uint(-1) // compile error: constant underflows uint )
只有常量可以是无类型的。当一个无类型的常量被赋值给一个变量的时候,无类型的常量将会被隐式转换为对应的类型,如果转换合法的话。
对于一个没有显式类型的变量声明(包括简短变量声明),常量的形式将隐式决定变量的默认类型,就像下面的例子:
i := 0 // untyped integer; implicit int(0)
r := '\000' // untyped rune; implicit rune('\000')
f := 0.0 // untyped floating-point; implicit float64(0.0)
c := 0i // untyped complex; implicit complex128(0i)
注意到,无类型整数的默认推导是 int,int 的大小不总是相同;但对于浮点和复数来说,float64 和 complex128 大小固定。go 的浮点数和复数的内存大小总是确定的。
4 复合数据类型
4.1 数组
长度固定(可以为 0),元素类型一致,下标从 0 开始
因为数组的长度是固定的,因此在 Go 语言中很少直接使用数组。和数组对应的类型是Slice(切片),它是可以增长和收缩的动态序列,slice 功能也更灵活
使用 len(数组)
取得元素个数
默认情况下,数组的每个元素都被初始化为元素类型对应的零值
你也可以使用 initializer_list(
标签:slice,函数,Point,int,BanG,MyGolang,func,类型,Dream From: https://www.cnblogs.com/Xiwon/p/17922417.html