|
0.背景
3年java开发背景(因此这篇文章的特点是:比较适合java程序员doge),业余时间有了解过一些go,如今加入字节团队主要技术栈是go,此篇主要结合go语言圣经和团队内go项目,总结一些基础知识。
另外文章中没有对if,switch ,for进行说明,看多了文中的例子,自然就会这部分了
1.程序结构
1.1.声明、赋值和类型
Go语言主要有四种类型的声明语句:var、const、type和func,分别对应变量、常量、类型和函数实体对象的声明
以下是一个 Go 语言程序结构的代码示例:
// 结构体声明type MyStruct struct { name string age int8 sex bool userType UserType sub *MyStruct keys []string feature map[string]any}// 类型 type UserType int64// 常量声明const ( SUPER UserType = 1)func main() { // 常量声明 const sexBool bool = false // 变量 var userName string // 变量赋值 userName = "username" //简短变量声明并赋值 userAge := 1 // map类型 userFeature := make(map[string]any) // map赋值 userFeature["key"] = "value" // 数组类型 var keys []string = make([]string, 20) //数组类型赋值 keys[0] = "!" // 结构体 var subStruct = MyStruct{age: 1, userType: 1, sex: false, name: "1"} myStruct := MyStruct{age: int8(userAge), userType: SUPER, sex: sexBool, name: userName, feature: userFeature, sub: &subStruct, keys: keys} }1.2.包和文件
包的概念和java中的包类似,不同的是导入包路径使用的是“/”进行分割
package mainimport ( "errors" "fmt")import "code.byted.org/life_service/chenxinglandingdemo/compare" // 导入自定义的包该包中有一个文件int_comparator.go,定义了一个Compare函数
type IntPriority interface { GetPriority() int}func Compare(first IntPriority, second IntPriority) (int, error) { if first == nil || second == nil { return 0, errors.New("null input") } firstPriority := first.GetPriority() secondPriority := second.GetPriority() if firstPriority == secondPriority { return 0, nil } if firstPriority > secondPriority { return 1, nil } return -1, nil}引入包后的使用例子
compare.Compare(&myStruct, &subStruct)其中大写开头的方法才被视为是public的,这点一开始让我觉得有点草率,但是似乎确实比java的private,protected,public用起来更爽一点
使用init方法可以对包文件的初始化,有点类似java类中的static静态代码块,但是无论该文件是否被使用,都会执行init,不像java只有使用到这个类执行类的初始化后才会执行static
package compareimport "fmt"func init() { fmt.Println("init_method_learn.go")}init方法的执行是依赖文件名称顺序的,因此如果A文件使用B文件中的方法是,也许B文件还没有调用init
2.基本数据类型
2.1 整形
Go语言同时提供了有符号和无符号类型的整数运算。这里有int8、int16、int32和int64四种截然不同大小的有符号整数类型,分别对应8、16、32、64bit大小的有符号整数,与此对应的是uint8、uint16、uint32和uint64四种无符号整数类型。
这里需要注意的是,int 和 uint 类型的大小取决于操作系统的位数。在32位操作系统中,int 和 uint 类型的大小通常为4字节(32位);在64位操作系统中,它们的大小通常为8字节(64位)。我看我们系统中一般是没有直接使用int和unit的。
2.2 浮点数
Go语言提供了两种精度的浮点数,float32和float64。它们的算术规范由IEEE754浮点数国际标准定义,该浮点数规范被所有现代的CPU支持。
和其他语言一样,float32和float64都有一定精度误差
var f float32 = 16777216// truefmt.Println(f+1 == f)var f64 float64 = 16777216// truefmt.Println(f64+0.000000000000001 == f64)在 IEEE 754 标准中,float32 类型的浮点数使用 32 位来存储,其中包括 1 位符号位(S)、8 位指数位(E)和 23 位尾数位(M)。对于数字 16777216,将其转换为二进制表示为 1000000000000000000000000。将其转换为 IEEE 754 格式的 float32 类型,具体步骤如下:
- 符号位 S:16777216 是正数,所以符号位 S 为 0。
- 指数位 E:将二进制数 1000000000000000000000000 右移 23 位,得到指数为 24。由于指数位的偏移量为 127,所以实际的指数值为 24 - 127 = -103。
- 尾数位 M:取二进制数 1000000000000000000000000 的后 23 位,即 00000000000000000000000,省略掉开头的 1。
因此,16777216 在 IEEE 754 存储中的表示为:0 10000001 00000000000000000000000。
对于数字 16777217,按照同样的步骤转换为 IEEE 754 格式的 float32 类型:
- 符号位 S:16777217 是正数,所以符号位 S 为 0。
- 指数位 E:将二进制数 1000000000000000000000001 右移 23 位,得到指数为 24。由于指数位的偏移量为 127,所以实际的指数值为 24 - 127 = -103。
- 尾数位 M:取二进制数 1000000000000000000000001 的后 23 位,即 000000000000000000000001,省略掉开头的 1。
所以,16777217 在 IEEE 754 存储中的表示也是:0 10000001 00000000000000000000000。
这就是为什么 16777216 和 16777217 在 IEEE 754 存储中看起来是一样的原因,它们的尾数位相同,而指数位也相同,只是在转换为十进制时,由于精度限制,会出现舍入误差,导致结果略有不同。
和java一样go也提供big来解决这个问题
// 使用 math/big 包进行高精度计算a := new(big.Float).SetPrec(128).SetFloat64(0.000000000000001)b := new(big.Float).SetPrec(128).SetFloat64(16777216)c := new(big.Float).Add(a, b)// 输出1,表示c大于b,符合预期fmt.Println(c.Cmp(b))2.3 bool 布尔
类似于java中的boolean,在 Go 语言中,布尔型数据只有true和false两个值,没有像 Java 中那样的Boolean类型。
2.4 字符串
func main() { str := "testStr测试字符串" // 输出22 ===>7+15(汉子3个字符) fmt.Println(len(str)) // 输出116 utf8编码 var u uint8 = str[0] fmt.Println(u) // 输出116 utf8编码 fmt.Println(str[0]) // testStr fmt.Println(str[0:7]) // testStr fmt.Println(str[:7]) //测试字符串 fmt.Println(str[7:]) // 测试字符串 fmt.Println(str[7:len(str)]) // 字符串循环 for i := range str { fmt.Println(string(str)) } }如上是字符串常用的一些操作,go中的字符串也是不可变的
让我比较迷惑的是str[7:]这种字符串的操作似乎是深拷贝,但是有一些文档也说是浅拷贝
3.复合数据类型
3.1 数组
// 声明,然后赋值ints := [10]int{}ints[1] = 1// 声明并赋值,没有赋值的index使用初值0var ints2 [20]int = [20]int{1, 2, 3, 4, 5}fmt.Println(ints2[19])fmt.Println(len(ints2))// 根据元素的个数来自动设置长度ints3 := [...]int{1, 2, 3, 4, 5}fmt.Println(len(ints3))ints4 := [...]int{99: -1}// 长度为100 = 99+1fmt.Println(len(ints4))ints5 := [5]int{1, 2, 3, 4, 5}// truefmt.Println(ints3 == ints5)其中我觉得比较有意思的是
如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,这时候我们可以直接通过==比较运算符来比较两个数组,只有当两个数组的所有元素都是相等的时候数组才是相等的。不相等比较运算符!=遵循同样的规则。
[5]int和[20]int是不同的类型
因此两个不太类型的数组是不可以比较的
以及也不能把[5]int类型的变量使用=赋值一个[20]int 类型的变量
- 数组作为参数
- 数组作为参数是值传递,这意味着下面clearArray1方法是无法清空数组内容的,使用指针的clearArray2是可以清空的(这也启发我们,比较占用内存的变量应该使用指针,因为指针作为参数值拷贝浪费的内存少,拷贝的是指针对象!)
- func clearArray1(array [5]int) { for i := range array { array = 0 }}func clearArray(array *[5]int) { for i := range array { array = 0 }}
- 因为[5]int和[20]int是不太同的数组类型,因此上面两个函数,都只能穿入长度为5的数组,这一点让我觉得有点恶心🤢
3.2 Slice 切片
Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。
数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,因此在Go语言中很少直接使用数组。
- 切片提供了访问数组子序列(或者全部)元素的功能
- 这一点有点像java中List的subList
- months := [...]string{"", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}// slice 可以访问所有元素,修改months[x:y]中的xy从而修改部分元素slice := months[1:13]
- slice的底层引用一个数组对象
这意味着修改切片内容会影响到数组原本内容,这一点也像java中ArrayList的subList,多个切片直接到修改会影响到对方,如下clear方法会影响到原数组内容
fmt.Println(months[1] == "")clear(slice)// true 修改切片会修改数组内容fmt.Println(months[1] == "")// 这里参数是一个切片类型func clear(array []string) { for i := range array { array = "" }}func clearArray(array [5]string) { for i := range array { array = "" }}还有一点比较有意思,clearArray参数类型是数组,clear参数类型是切片😂
这样看来数组有点鸡肋,数组不同长度是不同的类型,目前我感觉这样设计的好处是:
不同长度的数组在内存中占用的空间大小也不同。将它们视为不同的类型可以让编译器更好地进行内存管理和优化,例如在分配内存时可以根据数组的长度进行更精确的计算
- 一个slice由三个部分构成:指针、长度和容量。指针指向第一个slice元素对应的底层数组元素的地址,长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量,使用append方法,切片会自动扩容
- func appendSlice() { slice := make([]int, 3, 5) slice[0] = 1 slice[1] = 2 slice[2] = 3 //初始切片: [1 2 3], 长度: 3, 容量: 5 fmt.Printf("初始切片: %v, 长度: %d, 容量: %d\n", slice, len(slice), cap(slice)) slice = append(slice, 4) slice = append(slice, 5) //添加4,5: [1 2 3 4 5], 长度: 5, 容量: 5 fmt.Printf("添加4,5: %v, 长度: %d, 容量: %d\n", slice, len(slice), cap(slice)) slice = append(slice, 6) //添加6位置扩容后切片: [1 2 3 4 5 6], 长度: 6, 容量: 10 fmt.Printf("添加6位置扩容后切片: %v, 长度: %d, 容量: %d\n", slice, len(slice), cap(slice))}
- 使用make,可以创建一个只可以通过切片访问的数组
- // 元素类型、长度,默认容量=长度make([]T, len)// 指定容量,可用来避免重复扩容make([]T, len, cap)
指定容量的方式让我觉得有点鸡肋,我用ArrayList的时候较少关注容量(一般就初始化的时候指定长度),ArrayList也可以有容量的概念,这一定我觉得java封装的更好。
3.3 map
下面是创建map以及写入数据的方式
map1 := make(map[string]int)map1["name"] = 1map2 := map[string]int{ "name": 1,}//两种方法等价map类似于java中的HashMap,但是也有所不同
- 可以存储基本类型,并且基本类型作为value有初始值
- map1 := make(map[string]int)// 输出0,因为int类型初始值是0valueOfChenxing := map1["chenxing"]fmt.Println(valueOfChenxing)// map[xxx]第二个返回值是是否存在此key_, exists := map1["chenxing"]fmt.Println(exists)
- 如果是基础类型,可以直接对value进行操作,即使map中不存在此key
- // 操作后,map1中写入test=1这样的键值对map1["test"]++// 操作后,test2中写入test2=1这样的键值对map1["test2"] = map1["test2"] + 1
在java中需要使用compute方法实现类似效果
- map的遍历
- for key, value := range map1 { fmt.Println(key, value)}
- 复杂类型没有默认值
- newMap := make(map[string]map[string]bool)value := newMap["name"]// truefmt.Println(value == nil)
个人感觉go中这种map套map表达起来有点僵硬,也是我对java的尖括号比较中意
3.4 结构体
结构体类似于java的类
type Animal struct { age int color string}如上定义里Animal类型,和java类不同的是
- 一个属性是否导出取决于字段是否大写,这意味着另外一个包访问Animal时,是无法Animal.age,因为age是微导出的,修改为Age则是导出的
- 结构体定义一个结构体属性,需要使用指针
- type Animal struct { age int color string // 这是不被允许的,应该*Animal descendant Animal}
一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。(该限制同样适用于数组。)但是S类型的结构体可以包含*S指针类型的成员:
- 如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体将可以使用或!=运算符进行比较。相等比较运算符将比较两个结构体的每个成员
相当于比较两个结构体所有的成员变量值
- 结构体的方法不是定义在结构体中的
- type Animal struct { age int color string descendant *Animal spark string}func (animal *Animal) Spark() (string, error) { if animal == nil { return "", errors.New("null input") } return animal.spark, nil}
如上定义里一个Spark方法,在使用的时候可以
animal2 := Animal{age: 10}fmt.Println(animal2.Spark())
- 结构体的嵌套
- type Cat struct { Animal kind string}
- func main() { animal := Animal{age: 10} cat := Cat{kind: "1"} cat.Animal.age = 10 // 嵌套结构如何初始化 cat2 := Cat{kind: "1", Animal: Animal{age: 10}} fmt.Println(cat2) fmt.Println(cat) fmt.Println(animal) // 可以直接使用Animal的属性 fmt.Println(cat.age) // 也可以..的获取 fmt.Println(cat.Animal.age) fmt.Println(cat2 == cat)}
这一点如同java中的继承
外层的结构体不仅仅是获得了匿名成员类型的所有成员,而且也获得了该类型导出的全部的方法。这个机制可以用于将一些有简单行为的对象组合成有复杂行为的对象。组合是Go语言中面向对象编程的核心
这意味着Animal的Spark方法可以让cat使用
cat := Cat{kind: "1"}fmt.Println(cat.Spark())4.函数
函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。
func 函数名(形参列表) (返回值列表) { body}除了声明方式不同之外,下面说明一些java中方法不同的点
4.1 相同形参可以合并
func add(x, y int) int { return x+y;}// add 和add1等价func add1(x int, y int) int { return x + y} 当参数不多的时候,看起来是要简洁一些,但是如果参数很多类型很杂的时候,就有点不利于阅读了
4.2 函数可以具备多返回值
这一点常被用在返回正确结果or异常的情况
func div(x int, y int) (int, error) { if y == 0 { return 0, errors.New("division by zero") } return x / y, nil}4.3 函数是一级公民
- 可以赋值给变量:函数可以像普通的值一样被赋值给变量。
- 可以作为参数传递:能将函数作为参数传递给其他函数。
- 和java中的FunctionInterface一样,可以方便的进行回调等,下面是一个具体的使用例子
- func main() { ints := make([]int, 3, 10) ints[2] = 1 // [0 0 1] fmt.Println(ints) ints = filter(&ints, func(i int) bool { return i > 0 }) // [1] fmt.Println(ints)}func filter(array *[]int, filter func(int) bool) []int { newArray := []int{} for _, i := range *array { filterFlag := filter(i) if !filterFlag { continue } newArray = append(newArray, i) } return newArray}
- 可以从函数返回:函数本身也可以作为另一个函数的返回值。
- 可以做一些惰性求值
- 例如需要通过rpc拉取三个系统的内容,然后存储到数据,可以返回三个入库的func,然后在一个事务中执行
4.4 匿名函数
这一点在团队项目中常见于mysql事务提交和回滚
// 记录异常var err error// transaction := mysql.GetTransaction(ctx) 获取事物 // defer类似于java中的finallyif t := mysql.GetTransaction(ctx); t == nil {// 如果transaction==null 说明之前没有开启事务,下面这一行开启事务 ctx = mysql.Begin(ctx).Ctx // 匿名函数,使用defer 类似于java finnally defer func() { // 有异常那么回滚事务 if err != nil { _ = mysql.Rollback(ctx) } else { // 否则回滚事务 _ = mysql.Commit(ctx) } }()}ctx 类似java的threadLocal,类似于spring中的TransationSynchoronzationManager。
defer类似于java中的finally
这里其实类似于java中try catch,在catch中回滚,如果没有异常那么提交
可以看到匿名函数可以访问外部函数作用域定义的变量
4.5 defer
defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。
类似于java中的finaly
var i intfunc main() { r := invoke() fmt.Println("invoke return,", r) fmt.Println("invoked,", i)}func invoke() int { i = 1 defer deferAdd(&i) defer deferPrint() return i}func deferAdd(num *int) { *num = *num + 10 fmt.Println("deferAdd")}func deferPrint() { fmt.Println("deferPrint")}打印结果是
deferPrint
deferAdd
invoke return, 1
invoked, 11
这个例子可以看出
4.6 错误,panic异常,以及使用recover 恢复
- error:它是一种表示错误状态的类型。在 Go 中函数经常返回一个 error 对象来表示可能出现的错误情况。它和 Java 中的 Exception 有一些相似之处,都是用于处理程序运行中的非正常情况。
- panic:它用于表示非常严重的、不可恢复的错误情况,导致程序立即停止执行并开始展开调用栈。它有点类似于 Java 中的某些严重的系统级错误,但也不完全相同。
func main() { fmt.Println(Parse("!")) fmt.Println("main end")}func Parse(input string) (r int, err error) { r = 2 defer func() { if p := recover(); p != nil { r = 3 err = fmt.Errorf("internal error: %v", p) } }() makePanic() r = 1 return}func makePanic() { fmt.Println("makePanic before") panic("makePanic") fmt.Println("makePanic after")}下面代码的输出:
makePanic before // 没有输出 makePanic after,说明panic会终止后续逻辑
3 internal error: makePanic // 输出的是3,是defer这个分支改了返回的r
main end // 说明我们从panic中恢复了
5.方法
一个对象其实也就是一个简单的值或者一个变量,在这个对象中会包含一些方法,而一个方法则是一个和特殊类型关联的函数。一个面向对象的程序会用方法来表达其属性和对应的操作,这样使用这个对象的用户就不需要直接去操作对象,而是借助方法来做这些事情。
和java中的方法类似,支持对象.方法进行调用
在结构体这一节中我们定义一个Animal结构体,和Animal对应的Spark方法
type Animal struct { age int color string spark string}func (animal Animal) Spark() (string, error) { if animal == nil { return "", errors.New("null input") } return animal.spark, nil}上面的代码里那个附加的参数animal,叫做方法的接收器(receiver),早期的面向对象语言留下的遗产将调用一个方法称为“向一个对象发送消息”。这样写就可以使用Animal.Spark()进行调用了
5.1 使用基于指针的方法
func (animal *Animal) slienceWithPoint() { animal.spark = ""}func (animal Animal) slience() { animal.spark = ""}如上定义两个方法,其中slienceWithPoint是可以将animal的属性真正进行更改的,slience只能改变传入的字面值,只能改变副本(go是值传递的,传入指针的时候也是值传递,但是指针指向的地址是原始变量!)
结合下面这段代码可以更好的理解,这也是go方法和java方法一个较大的不同
func main() { animal := Animal{spark: "wangwang"} // wangwang <nil> fmt.Println(animal.Spark()) animal.slience() // wangwang ===>只改变脸传入的副本,而没有改变原变量 fmt.Println(animal.spark) // 使用基于指针的方法 animal.slienceWithPoint() // true ==> 说明改变脸原对象 fmt.Println(animal.spark == "") }在我们系统的代码中也可以看到大部分方法都是基于指针的
5.2 nil也可以调用方法
var nilAnimal Animal// 输出的是truefmt.Println(nilAnimal.SparkWithOutCheck() == "")fmt.Println("end")func (animal *Animal) SparkWithOutCheck() string { return animal.spark}如上 使用nil调用SparkWithOutCheck,返回的是空白字符串,NPE没了,笑死
5.3 通过嵌入结构体来实现方法的继承
如结构体这一节中说到的,go中的继承是通过组合来实现的
type Cat struct { Animal // cat继承Animal kind string}cat :=Cat{ kind: "1", Animal: Animal{ spark: "miaomiao", },}// cat具备Animal中的方法// 输出 miaomiaofmt.Println(cat.SparkWithOutCheck())当然cat也可以复写Animal中的方法
func (c Cat) SparkWithOutCheck() string { return "kuakua"}再次使用cat.SparkWithOutCheck()将打印kuakua
6.接口
如下我们定义一个接口类型,并定义一个使用改接口类型的方法
type IntPriority interface { GetPriority() int GetPriority2(int) int}// 使用接口作为参数进行比较func Compare(first IntPriority, second IntPriority) (int, error) { if first == nil || second == nil { return 0, errors.New("null input") } firstPriority := first.GetPriority() secondPriority := second.GetPriority() if firstPriority == secondPriority { return 0, nil } if firstPriority > secondPriority { return 1, nil } return -1, nil}6.1 只有实现了接口的所有方法才算实现了接口
这一点和java很类型,但是go没有implement关键字,好在Goland实现了代码提示
func init() { fmt.Println("init_method_learn.go") field1 = "test" entity := Entity{age: 1} var i IntPriority = &entity fmt.Println(i)}type Entity struct { age int}func (e *Entity) GetPriority() int { return e.age}由于我们Entity结构只实现了GetPriority方法,下面代码无法通过编译
entity1 := Entity{age: 1}entity2 := Entity{age: 2}// 编译错误fmt.Println(Compare(entity2, entity1))// 编译错误var i IntPriority = &entity16.2 接口是引用类型,结构体是值类型
即使Entity实现了IntPriority中的所有方法,在调用Compare的时候,也需要使用&Entity获取指针传入,
当需要在函数内部修改传入的参数并且希望这些修改对函数外部可见时,需要传入指针而不是对象。这是因为 Go 语言中的结构体是值类型,而不是引用类型。当传递结构体对象时,函数内部会复制一份结构体的值,对其进行的修改不会影响原始的结构体对象。
接口作为引用类型的好处:
- 灵活性和动态性:可以方便地在运行时将不同的具体类型与接口关联,实现多态行为,使代码能够适应不同的实现而无需大规模修改。
- 解耦性:通过接口来定义行为规范,调用方只需关注接口定义,而不关心具体实现细节,增强了模块之间的解耦。
结构体作为值类型的好处:
- 简单和直观:结构体的赋值和传递行为相对简单明确,符合人们对一般数据结构的理解和使用习惯。
- 数据独立性:每个结构体实例都是独立的数据块,修改一个结构体变量不会影响其他变量,有利于代码的安全性和可维护性。
- 高效传递:在一些场景下,值传递可以避免不必要的指针间接操作,提高性能,并且避免一些因指针使用不当带来的问题。
6.3 类型断言
通过下面类似的语法,可以判断一个值是否是某个类型,如果不满足那么会抛出panic
func main() { m := map[string]interface{}{} var i int = 10 m["name"] = i value := m["name"] i2 := value.(int) // 10 fmt.Println(i2) //panic: interface conversion: interface {} is int, not string i3 := value.(string) fmt.Println(i3)}另外还支持返回是否是该类型,如下面的ok,是一个bool类型,这样能避免panic的抛出
i3, ok := value.(string)fmt.Println(i3, ok)6.4 类型分支
如下我们编写一个addOne方法尝试支持所有数值类型的➕1操作
func addOne(x interface{}) any { if x == nil { return "NULL" } else if intValue, ok := x.(int); ok { return intValue + 1 } else if uintValue, ok := x.(uint); ok { return uintValue + 1 } else { panic(fmt.Sprintf("unexpected type %T: %v", x, x)) }}可以通过类型断言来判断是否是某一个类型
也可以通过switch来判断
func addOne2(value interface{})any{ switch x := value.(type) { case nil: return "NULL" case int: i2 := value.(int) return i2+ 1// x has type interface{} here. default: panic(fmt.Sprintf("unexpected type %T: %v", x, x)) }} |
|