摘要:本文由葡萄城技术团队于思否原创并首发。转载请注明出处:葡萄城官网,葡萄城为开发者提供业余的开发工具、解决方案和服务,赋能开发者。
前言
本文次要通过值传递和指针、字符串、数组、切片、汇合、面向对象(封装、继承、形象)和设计哲学 7 个方面来介绍 GO 语言的个性。
文章目录:
1.Go 的前世今生
1.1Go 语言诞生的过程
1.2 逐渐成型
1.3 正式公布
1.4 Go 装置领导
2.GO 语言非凡的语言个性
2.1 值传递和指针
2.2字符串
2.3 数组
2.4 切片
2.5汇合
2.6 面向对象
2.6.1 封装
2.6.2 继承
2.6.3 形象
2.7 设计哲学
2.7.1. Go 没有默认的类型转换
2.7.2. Go 没有默认参数,同样也没有办法重载
2.7.3. Go 不反对 Attribute
2.7.4. Go 没有 Exception
Rob Pike 和平常一样启动了一个 C++ 我的项目的构建,依照他之前的教训,这个构建应该须要继续 1 个小时左右。这时他就和 Google 公司的另外两个共事 Ken Thompson 以及 Robert Griesemer 开始吐槽并且说出了本人想搞一个新语言的想法。过后 Google 外部次要应用 C++ 构建各种零碎,但 C++ 复杂性微小并且原生短少对并发的反对,使得这三位大佬苦恼不已。
第一天的闲聊初有功效,他们迅速构想了一门新语言:可能给程序员带来高兴,可能匹配将来的硬件发展趋势以及满足 Google 外部的大规模网络服务。并且在第二天,他们又碰头开始认真构思这门新语言。第二天会后,Robert Griesemer 收回了如下的一封邮件:
能够从邮件中看到,他们对这个新语言的冀望是:在 C 语言的根底上,批改一些谬误,删除一些诟病的个性,减少一些缺失的性能。比方修复 Switch 语句,退出 import 语句,减少垃圾回收,反对接口等。而这封邮件,也成了 Go 的第一版设计初稿。
在这之后的几天,Rob Pike 在一次开车回家的路上,为这门新语言想好了名字 Go。在他心中,”Go”这个单词短小,容易输出并且能够很轻易地在其后组合其余字母,比方 Go 的工具链:goc 编译器、goa 汇编器、gol 连接器等,并且这个单词也正好合乎他们对这门语言的设计初衷:简略。
https://golang.google.cn/
抉择对应的装置版本即可(倡议抉择.msi 文件)。
2. 查看是否装置胜利 + 环境是否配置胜利
关上命令行:win + R 关上运行框,输出 cmd 命令,关上命令行窗口。
命令行输出 go version 查看装置版本,显示下方内容即为装置胜利。
[外链图片转存失败, 源站可能有防盗链机制, 倡议将图片保留下来间接上传(img-nhDTdQKK-1689067489257)(media/54576b6c7593e8c73a97975be0910b4b.png)]
2.Go 语言非凡的语言个性
2.1 值传递和指针
Go 中的函数参数和返回值全都是按值传递的。什么意思呢?比方下述的代码:
type People struct {name string}
func ensureName(p People) {p.name = "jeffery"}
func main() {
p := People{name: ""}
ensurePeople(p)
fmt.Println(p.name) // 输入:""
}
为啥下面这段代码没有把 p 的内容改成“jeffery”呢?因为 Go 语言的值传递个性,ensureName 函数内收到的 p 曾经是 main 函数中 p 的一个正本了。这就和 C\# 中把 p 改为一个 int 类型失去的后果一样。
那怎么解决呢?用指针。
不晓得其他人怎么样,当我最开始学习 Go 的时候发现须要学指针的时候霎时回想起了大学期间被 C 和 C++ 指针折磨的那段苦楚回顾,所以我本能的对指针就有一种排挤感,尽管 C\# 中也能够用指针,然而如果不写底层代码,可能写 10 年代码都用不到一次。
不过还好,Go 中对指针的应用进行了简化,没有简单的指针计算逻辑,仅晓得两个操作就能够很轻松的用好指针:
- “*“: 取地址中的内容
- “&”: 取变量的地址
var p \*People = \&People{name: "jeffery",}
上述代码中,我创立了一个新的 People 实例,并且通过”&”操作获取了它的地址,把它的地址赋值给了一个 *People 的指针类型变量 p。此时,p 就是一个指针类型,如果依照 C 或者 C++,我是无奈间接操作 People 中的字段 name 的,然而 Go 对指针操作进行了简化,我能够对一个指针类型变量间接操作其内的字段,比方:
func main() {fmt.Println(p.name) // 输入:jeffery
fmt.Println(\*(p).name) // 输入:jeffery
}
上述的两个操作是等价的。
有了指针,咱们就能够很轻松的模拟出 C\# 那种 按援用传递参数 的代码了:
type People struct {name string}
func ensureName(p \*People) {p.name = "jeffery"}
func main() {
p := \&People{name: ""}
ensurePeople(p)
fmt.Println(p.name) // 输入:jeffery
}
2.2 字符串
在 C\# 中字符串其实是 char 类型的数组,是一个非凡的调配在栈空间的援用类型。
而在 Go 语言中,字符串是 值类型 ,并且 字符串是一个整体。也就是说咱们不能批改字符串的内容,从上面的例子能够很分明的看出这一概念:
var str = "jeffery";
str[0] = 'J';
Console.WriteLine(str); // 输入:Jeffery
上述的语法在 C\# 中是成立的,因为咱们批改的其实是字符串中的一个 char 类型,而 Go 中这样的语法是会被编译器报错的:
str := "jeffery"
str[0] = 'J' // 编译谬误:Cannot assign to str[0]
然而咱们能够用数组 index 读取对应字符串的值:
s := str[0]
fmt.Printf("%T", s) // uint8
能够看到这个返回值是 uint8,这是为啥呢?其实,在 Go 中,string 类型是由一个名为 rune 的类型组成的,进入 Go 源码看到 rune 的定义就是一个 int64 类型。这是因为 Go 中把 string 编译成了一个一个的 UTF8 编码,每一个 rune 其实就是对应 UTF8 编码的值。
此外,string 类型还有一个坑:
str := "李正龙"
fmt.Printf("%d", len(str))
len()函数同样也是 go 的内置函数,是用来求汇合的长度的。
下面这个例子会返回 9,这是因为中文在 Go 中会编译为 UTF-8 编码,一个汉字的编码长度就 3,所以三个汉字就成了 9,然而也不肯定,因为一些非凡的汉字可能占 4 个长度,所以不能简略用 len() / 3 来获取文字长度。
因而,汉字求长度的办法应该这样做:
fmt.Println(utf8.RuneCountInString("李正龙"))
2.3 数组
Go 中的数组也是一个我感觉设计的有点过于底层的概念了。根底的用法和 C\# 是雷同的,然而细节区别还是很大的。
首先,Go 的数组也是一个 值类型 ,除此之外,因为”严格地“遵循了数组是一段间断的内存的联合这个概念, 数组的长度是数组的一部分。这个概念也很重要,因为这是间接区别于切片的一个特色。而且,Go 中的数组的长度只能是一个常量。
a := [5]int{1,2,3,4,5}
b := [...]{1,2,3,4,5}
lena := len(a)
lenb := len(b)
上述是 Go 中数组的两个比拟惯例的初始化语法,数组的长度和字符串一样,都是通过 len()内置函数获取的。其余的应用和 C\# 基本相同,比方能够通过索引取值赋值,能够遍历,不能够插入值等。
2.4 切片
与数组对应的一个概念,就是 Go 中独有的切片 Slice 类型。在日常的开发中简直很少能用失去数组,因为数组没有扩大能力,比方 C\# 中咱们也简直用不到数组,能用数组的中央根本都用 List\<T\>。Slice 就是 List 的一种 Go 语言实现,它是一个 援用类型,次要的目标是为了解决数组无奈插入数据的问题。其底层也是一个数组,只不过它对数组进行了一些封装,退出了两个指针别离指向数组的左右边界,就使得 Slice 有了能够减少数据的性能。
s1 := []int{1,2,3,4,5}
s2 := s1[1:3]
s3 := make([]int, 0, 5)
下面是 Slice 的三种罕用的初始化形式。
- 能够看到切片和数组的惟一区别就是没有了数组定义中的数量
- 能够基于一个去切片去创立另一个切片,其前面的数字的含意就是目前业界通用的左蕴含右关闭
- 能够通过 **make()** 函数创立一个切片
make()函数感觉能够随同 Go 开发者的毕生,Go 的三个援用类型都是通过 make 函数进行初始化创立的。对切片来说,第一个参数示意切片类型,比方上栗就是初始化一个 int 类型的切片,第二个参数示意切片的长度,第三个参数示意切片的容量。
想切片中插入数据须要应用到 append()函数,并且语法非常诡异,能够说是离谱到家了:
s := make([]int)
s = append(s, 12345) // 这种追加值还须要返回给原汇合的语法真不知道是哪个小蠢才想到的
这里呈现了一个新的概念,切片的 容量。咱们晓得数组是没有容量这个概念的(其实是有的,只不过容量就是长度),而切片的容量其实就相似于 C\# 中 List\<T\> 的容量(我晓得大部分 C\#er 在应用 List 的时候基本不会去关怀 Capacity 这个参数),容量示意的是底层数组的长度。
容量能够通过 cap()函数获取
在 C\# 中,如果 List 的数据写满了底层数组,那会产生扩容操作,须要新开拓一个数组将原来的数据复制到新的数组中,这是很消耗性能的一个操作,Go 中也是一样的。因而在日常开发应用 List 或者切片的时候,如果能提前确定容量,最好就是初始化的时候就定义好,防止扩大导致的性能损耗。
2.5 汇合
Go 中除了把 List 内置为切片,同样也把 Dictionary\<TKey, TValue\> 内置为了 map 类型。map 是 Go 中三个 援用类型 的第二个,其创立的形式和切片雷同,也须要通过 make 函数:
m := make(map[int]string, 10)
从字面意思咱们就能够晓得,这句话是创立了一个 key 为 int,value 为 string,初始容量是 10 的 map 类型。
对 map 的操作没有像 C\# 那么简单,get,set 和 contains 操作都是通过 [] 来实现的:
m := make(map[string]string, 5)
// 判断是否存在
v, ok := m["aab"]
if !ok {// 阐明 map 中没有对应的 key}
// set 值,如果存在反复 key 则会间接替换
m["aab"] = "hello"
// 删除值
delete(m, "aab")
这里要说个坑,尽管 Go 中的 map 也是能够遍历的,然而 Go 强制将后果乱序了,所以每次遍历不肯定拿到的是雷同程序的后果。
2.6 面向对象
2.6.1 封装
终于说到面向对象了。仔细的同学必定曾经看到了,Go 外面居然没有封装管制关键字 public,protected 和 private!那我这面向对象第一准则的封装性怎么搞啊?
Go 语言的封装性是通过 变量首字母大小写管制的(对重度代码洁癖患者的我来说,这几乎是天大的福音,我再也不必看到那些首字母小写的属性了)。
// struct 类型的首字母大写了,阐明能够在包外拜访
type People struct {
// Name 字段首字母也大写了,同理包外可拜访
Name string
// age 首字母小写了,就是一个包内字段
age int
}
// New 函数大写了,包外能够调到
func NewPeople() People {
return People{
Name: "jeffery",
age: 28
}
}
2.6.2 继承
封装搞定了,继承怎么搞呢?Go 里如同也没有继承的关键字 extends 啊?Go 齐全以设计模式中的优先组合而非继承的设计思维设计了复用的逻辑,在 Go 中 没有继承,只有组合。
type Animal struct {
Age int
Name string
}
type Human struct {
Animal // 如果默认不定义字段的字段名,那 Go 会默认把组合的类型名定义为字段名
// 这样写等同于:Animal Animal
Name string
}
func do() {
h := \&Human{Animal: Animal{Age: 19, Name: "dog"},
Name: "jeffery",
}
h.Age = 20
fmt.Println(h.Age) // 输入:20,能够看到如果本身没有组合构造体雷同的字段,那能够省略子结构体的调用间接获取属性
fmt.Println(h.Name) // 输入:jeffery,对于有雷同的属性,优先输入本身的,这也是多态的一种体现
fmt.Println(h.Animal.Name)// 输入:dog,同时,所组合的构造体的属性也不会被扭转
}
这种组合的设计模式极大的升高了继承带来的耦合,单就这一点来说,我认为是完满的银弹。
2.6.3 形象
在解说关键字的局部咱们就曾经看到了,Go 是有接口的,然而同样没有实现接口的 implemented 关键字,那是因为 Go 中的接口全部都是 隐式实现 的。
type IHello interface {sayHello()
}
type People struct {}
func (p \*People) sayHello() {fmt.Println("hello")
}
func doSayHello(h IHello) {h.sayHello()
}
func main() {p := \&People{}
doSayHello(p) // 输入:hello
}
能够看到,上例中的构造体 p 并没有和接口有任何关系,然而却能够失常被 doSayHello 这个函数援用,次要就是因为 Go 中的所有接口都是隐式实现的。(所以我感觉真的有可能呈现你写着写着忽然就实现了某个依赖包的某个接口的状况)
此外,这里看到了一个不一样的语法,函数关键字 func 之后没有间接定义函数名称,而是退出了一个构造体 p 的一个指针。这样的函数就是构造体的函数,或者更直白一点就是 C\# 中的办法。
在默认状况下,咱们都是应用指针类型为构造体定义函数,当然也能够不必指针,然而在那种状况下,函数所更改的内容就和原构造体齐全不相干了。所以个别也遵循一个 无脑用指针 的准则。
好了,封装、继承和形象都有了,至于多态,在继承那里曾经看到了,Go 也是优先匹配本身的雷同函数,如果没有才回去调用父构造体的函数,因而默认状况下的函数都是被重写之后的函数。
2.7 设计哲学
Go 语言的设计哲学是 less is more。这句话的意思是 Go 须要简略的语法,其中简略的语法也包含 显式大于隐式(接口类型真是满头问号)。这是什么意思呢?
2.7.1. Go 没有默认的类型转换
var i int8 = 1
var j int
j = i // 编译报错:Cannot use 'i' (type int8) as the type int
还有一个例子就是 string 类型不能默认和 int 等其余类型拼接,比方输出 ”n 你好 ” + 1 在 Go 中同样会报编译谬误。起因就是 Go 的设计者感觉这种都是隐式的转换,Go 须要简略,不应该有这些。
2.7.2. Go 没有默认参数,同样也没有办法重载
这也是一个很让人恼火语言个性。因为不反对重载,写代码时就不得不写大量可能反复然而名字不雷同的函数。这个个性也是有开发者专门问过 Go 设计师的,给出的回复就是 Go 的设计指标就是简略,在简略的大前提下,局部冗余的代码是能够承受的。
2.7.3.Go 不反对 Attribute
和目前没有泛型不同,Go 的泛型是一个正在开发的性能,是还没来得及做的。而个性 Attribute 也就是 Java 中的注解,在 Go 中是被明确阐明不会反对的语言个性。
注解能在 Java 中带来怎么弱小的性能呢?举一个例子:
在大型互联网都转向微服务架构的时代,分布式的多段提交,分布式事务就是一个比拟大的技术壁垒。以分布式事务为例,多个微服务很可能都不是一个团队开发的,也可能部署在世界各地,而如果一个操作须要回滚,其余所有的微服务都须要实现回滚的机制。这里不光波及简单的业务模型,还有更简单的数据库回滚策略(什么 2PC 啊,TCC 啊每一个策略都能够当一门独自的课来讲)。
这种货色如果要从头开发那简直是很难思考全面的。更别提这样的简单代码再 耦合 到业务代码中,那代码会变得十分难看。都不说分布式事务了,简略的一个内存缓存,咱们用的都很凌乱,在代码中会常常看到先读取缓存在读取数据库的代码,和业务齐全耦合在一起,齐全无奈保护。
而 Spring Cloud 中,代码的使用者能够通过一个简略的注解(也就是 C\# 的个性)@Transactional,那这个办法就是反对事务的,使这种简单的技术级代码齐全和业务代码 解耦,开发者齐全依照失常的业务逻辑写业务代码即可,齐全不必管事务的一些问题。
然而,Go 的设计者同样认为注解会重大影响代码使用者对一个调用的应用心智,因为加了一个注解,就能够导致一个函数的性能齐全不一样,这与 Go 显式大于隐式的设计理念相违反,会重大减少使用者的心智累赘,不合乎 Go 的设计哲学(哎,就离谱…)
2.7.4. Go 没有 Exception
在 Go 中没有异样的概念,相同地提供了一个 error 的机制。对 C\# 来说,如果一段代码运行存在问题,那咱们能够手动抛出一个 Exception,在调用方能够 捕捉 对应的异样进行之后的解决。而 Go 中没有异样,代替的计划是 error 机制。什么是 error 机制呢?还记得之前讲过的 Go 的简直所有的函数都有多个返回值吗?为啥要那么多的返回值呢?对,就是为了接管 error 的。比方下述代码:
func sayHello(name string) error {
if name == "" {return errors.New("name can not be empty")
}
fmt.Printf("hello, %s\\n", name)
return nil
}
// invoker
func main() {if err := sayHello("jeffery"); err != nil {// handle error}
}
这样的 error 机制须要保障所有的代码运行过程中都不会异样解体,每个函数到底执行胜利了没有,须要通过函数的返回错误信息来判断,如果一个函数调用的返回后果的 error == nil,阐明这段代码没问题。否则,就要手动解决这个 error。
这样就有可能导致一个重大的结果:所有的函数调用都须要写成
if err := function(); err != nil
这样的构造。这样的结果简直是灾难性的(这也是为啥 VS2022 反对了代码 AI 补全性能后,网上的热评都是利好 Gopher)这种 error 的机制也是 Go 被黑的最惨的中央。
那这时候必定有小伙伴说了,那我就是不解决搞一个相似于 1 / 0 这样的代码会怎么样呢?
如果写了相似于上述的代码,那最终会引发一个 Go 的 panic。在我目前通俗的了解中,panic 其实才是 C\# 中 Exception 的概念,因为程序运行遇到 panic 后就会彻底解体了,Go 的设计者在最开始的设计中预计是认为所有的谬误都应该用 error 解决,如果引发了 panic 那阐明这个程序无奈应用了。因而 panic 其实是一个无法挽回的谬误的概念。
然而,大型的我的项目中,并不是本人的代码写的十拿九稳就没有 panic 了,很可能咱们援用的其余包干了个什么咱们不晓得的事就 panic 了,比方最典型的一个例子:Go 的 httpRequest 中的 Body 只能读取一次,读完就没有了。如果咱们应用的 web 框架在解决申请时把 Body 读了,咱们再去读取后果很有可能 panic。
因而,为了解决 panic,Go 还有一个 recover()的函数,个别的用法是:
func main() {panic(1)
defer func() {if err := recover(); err != nil {fmt.Println("boom")
}
}
}
其实 Go 有一个弱小的竞争者——Rust,Rust 是 Mozilla 基金会在 2010 年研发的语言,和 Go 是以 C 语言为根底开发的相似,Rust 是以 C++ 为基准进行开发的。所以当初社区中就有 Go 和 Rust 两拨营垒在互相争执,吵得呶呶不休。当然,万物没有银弹,所有的事物都应该以辩证的思维去学习了解。
好了,看完了下面那些 Go 的语法之后,必定也会手痒写一点 Go 的代码练习练习,加深记忆。正好,那就来实现一个 Go 官网中的一个小例子,本人入手实现一下这个计算 Fibonacci 数列第 N 个数的接口吧。
type Fib interface {
// CalculateFibN 计算斐波那契数列中第 N 个数的值
// 斐波那契数列为:前两项都是 1,从第三项开始,每一项的值都是前两项的和
// 例如:1 1 2 3 5 8 13 ...
CalculateFibN(n int) int
}
因为篇幅所限,这个小练习的答案我放在了本人的 gitee 中,欢送大家拜访。