Golang学习笔记(四):Go原始设计思想及核心特性

1 概述

本人学习Golang的方式和一般人不同,采用了先从大处着手,先从大的方面把握Go编程的总体框架和风格,因此首先就有了前面的三篇文章:

通过上面三篇文章,同时你又有C、C++基础,那么恭喜你!你应该可以开始实战编程了!当然,在编程的过程中你可能还需要不断的问问度娘。

本篇文章还是力图从大处着手,通过对Go设计团队介绍、Go设计团队人员的一些演讲摘要以及总结一些核心特性让你对Go有个更宏观的认识。

2 Go语言设计团队堪称豪华

2.1 Ken Thompson

1943年出生于美国新奥尔良,1966年加入了贝尔实验室,不久独自设计了B语言,并用B语言开发了Unix操作系统的第一个版本,1971年与丹尼斯·里奇共同发明了C语言,并在1973年两人共同用C语言重写了UNIX,1983年图灵奖得主。2006年进入谷歌,2007年和Rob Pike、Robert Griesemer三人一起设计出了Go语言。

在2011年的一次采访中,Ken幽默地谈到设计Go语言的初衷是他们很不喜欢C++,因为C++中充满了大量的垃圾特性。

2.2 Rob Pike

早年在贝尔实验室和Ken Thompson结对编程,公开场合表示Ken是他的导师,也是Unix的先驱,是贝尔实验室最早和Ken Thompson以及 Dennis M. Ritche 开发Unix的猛人,和Ken Thompson一起发明了UTF-8。Go设计团队第一任Leader。

如今虽然和Ken一样已退休并被谷歌尊养起来了,但Rob Pike仍旧活跃在各个Go论坛组中,适当地发表自己的意见。后面会引用他2012年在旧金山给Go SF的演讲稿的部分内容让我们对Go的原始设计思想有个大的了解。

顺便说一句,Go语言的地鼠吉祥物是由Rob Pike的媳妇Renee French设计的。

2.3 Robert Griesemer

Robert Griesemer是Go编程语言的最初三名设计者之一。在此之前,Robert致力于Google V8 JavaScript引擎的代码生成、特定领域语言Sawzall、Java热点虚拟机和Strongtalk系统的设计和实现。他曾经为Cray Y-MP编写了一个矢量编译器,并为APL编写了一个解释器。

目前主要维护Go白皮书和代码解析器等。

2.4 Russ Cox

Russ Cox毕业于麻省理工和哈佛大学,曾在贝尔实验室参与过Plan 9 和 Unix操作系统的开发。在Go开始设计不久,Russ Cox 和Ian Lance Taylor 也加入了 Go 语言的设计团队,由三人团队变成了5人团队。他们 5 人一起开发了两款编译器和一个标准库,为 2009 年 11 月 10 日 Go 语言以开源方式发布打下了基础。

Russ Cox为目前Go团队的leader,他非常年轻,代码提交量排第一。目前很多拿不定主意的决策都是Russ Cox最后拍板。

2.5 Ian Lance Taylor

在GCC的世界中,没有人比Ian更火。在GCC maillist中,Ian的身影呈现在前端中端和后端中,粗略估计1/3的帖子他都给出有价值的回复,绝对不是灌水。

Google推出了go编程语言,Ian基于语言规范独立完成了GCC前端。他写了Gold连接器(比binutils快),最早的GCC MIPSR4000后端,是GCC过程间优化LTO的主要设计者之一,为PostgreSQL数据库增加功能。他曾经是Zembu公司的创始人,目前在Google工作。gccgo编译器的作者和cgo工具链维护者。活跃于各个go订阅组,耐心解答各种问题。

2.6 Brad Fitzpatrick

Brad Fitzpatrick是 LiveJournal.com 的创始人,还是 memcached 等诸多自由软件项目的作者,2007 年选择加入 Google,OpenID、PubSubHubbub 等项目均有他的贡献。2011年在为 Android 项目效力一年半多后,正式成为 Go 语言项目的全职“地鼠”(gopher),目前是net/http标准库包的主要维护者。

当然,Go团队还有很多牛人,就不一一去人肉了 😃

3 Go语言之父Rob Pike说过……

下面通过对Google首席工程师、Go语言之父Rob Pike在旧金山给Go SF的演讲稿摘要,让我们一睹Go语言最初的设计思想:

  • 以C语言为原型,修补部分明显的缺陷,去掉垃圾功能,添加一些缺失的功能。
  • 我们也加入了一下C和C++里没有的东西,比如slice和map、复合文本(composite literal)、顶层文件表达式(这是一个大问题,但经常被忽视)、反射(reflection)、垃圾回收等等,我们还非常自然地加入了并发性。
  • 命名为“Go”,好处有很多,这名字非常简短,容易拼写。工具可以叫做:goc、gol、goa。如果有可交互的调试器/解释器也可以简单地叫它“Go”,后缀名用 .go。
  • 定义空接口语法:interface{}。所有接口都通过这个实现,因此可以用它代替void*。
  • 从C++和Java转向Go的程序员很怀念使用类(class)编程的方式,特别是继承和子类这样的概念。也许关于“class”我只是一个门外汉,但我真没发现模板有什么好处。真正重要的是这些类能为你做什么而不是它们之间是什么关系!
  • 如果说C++和Java是关于类的层次和分类,那么Go的核心思想就是组合(composition)。
  • Go有一个非常不同寻常但是很简单的类型组成方式:嵌入。这些组合技术正是Go的独特之处,与C++或者Java程序截然不同。
  • 在Go语言的首次启航中,我得到了一个“我不能在没有泛型的环境下工作”的反馈,这真是一个奇怪的言论。(作者注:Go 2中有在考虑泛型)

4 Go从C和C++简化的功能

以下也是从Rob Pike在旧金山给Go SF的演讲稿中摘出来的,有些地方不是很懂,但可以看出Go和C、C++大概的区别,我下一步计划好好整理一下Go和C的区别,计划在下一章节写出。

以下是Go从C和C++简化的功能:

  • 规范的语法(不需要符号表来解析)
  • 垃圾回收(独有)
  • 无头文件
  • 明确的依赖
  • 无循环依赖
  • 常量只能是数字
  • int和int32是两种类型
  • 字母大小写设置可见性(作者注:包中首字母大写的全局变量和方法可被其他包调用)
  • 任何类型(type)都有方法(不是class)
  • 没有子类型继承(不是子类)
  • 包级别初始化以及明确的初始化顺序
  • 文件被编译到一个包里
  • 包级全局变量可按任意顺序展示(package-level globals presented in any order—暂时搞不明白什么意思
  • 没有数值类型转换(常量起辅助作用)(作者注:golang 类型转换只能显性转换 不能自动转换)
  • 接口隐式实现(没有“implement”声明)
  • 嵌入(不会提升到超类)
  • 方法按照函数声明(没有特别的位置要求)
  • 方法即函数
  • 接口只有方法(没有数据)
  • 方法通过名字匹配(而非类型)
  • 没有构造函数和析构函数
  • i++是语句,不是表达式
  • 没有++i
  • 赋值不是表达式
  • 明确赋值和函数调用中的计算顺序(没有“sequence point”)
  • 没有指针运算(作者注:Go 有指针,但是没有指针运算,你不能用指针变量遍历字符串的各个字节)
  • 内存一直以零值初始化
  • 局部变量取值合法
  • 方法中没有“this”
  • 分段的堆栈
  • 没有静态和其它类型的注释 (这里不是很懂
  • 没有模板
  • 没有异常(作者注:内置error类型,没有try…catch…)
  • 内建string、slice和map
  • 数组边界检查

5 Go语言的核心特性

Go是一种编译型,并发型,并具有垃圾回收功能的编程语言。Go语言的语法接近C语言。与C++相比,Go语言并不包括如继承、泛型、断言、虚函数等功能,但增加了slice型、并发、管道、垃圾回收、接口(interface)等特性的语言级支持。当然,Go 对于泛型的态度还是开放的,有说法在Go 2中可能支持,目前Go语言对泛型有其替代方案:interface{},一般叫空接口,空接口内部没有声明任何方法,因此指代Go中的任何类型,但声明为空接口类型时,指代接收任何数据类型,在使用通过空接口传递的数据时,通过类型断言的方式接收数据并做进一步处理。这种做法把运行时出错的可能性提前到编译时发现,如有运行时错误也能等到友好的处理。

Go语音的特性很多,我阅读众多博文之后总结了以下几个是最重要的核心特性。

5.1 并发编程

在当今这个互联网时代,并发编程的意义不言而喻。当然,很多语言都支持多线程、多进程编程,但遗憾的是,实现和控制起来并不是那么令人感觉轻松和愉悦。Golang不同的是,语言级别支持协程(goroutine)并发(协程又称微线程,比线程更轻量、开销更小,性能更高),操作起来非常简单,语言级别提供关键字(go)用于启动协程,并且在同一台机器上可以启动成千上万个协程。协程经常被理解为轻量级线程,一个线程可以包含多个协程,共享堆不共享栈。协程间一般由应用程序显式实现调度,上下文切换无需下到内核层,高效不少。

Go语言在并发编程方面非常简洁,只需在调用的方法前(非赋值情况下)或代码块前,加上“go”关键字即可启动一个协程,如下:

package main

import (
    "fmt"
    "time"
)

func testGoroutine(pausems int) {
    for i := 1; i <= 10; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Printf("testGoroutine(%d): %d\n", pausems, i)
    }
}

func main() {
	//启动了10个并发协程
    for i:=1; i<=10; i++{
		go testGoroutine(i)
	}
	//必须确保主进程没有过快退出,否则主进程退出,所有协程也会被强行终止
    time.Sleep(5*time.Second)
    fmt.Println("main function out")
}

协程间可通讯,而且golang中实现协程间通讯有两种:1)共享内存型,使用全局变量+mutex锁来实现数据共享;2)消息传递型,使用channel机制进行异步通讯。使用也很简单这里就不详细展开了。

5.2 面向接口编程

这一特性有些博文总结为“非侵入式接口”,但在详细说明时,写得不是特别清楚。但使用“面向接口编程”这一提法时,大部分博文都能说得比较清楚。我把“面向接口编程”列为第二个重要特性,实际上应该列为第一特性,只是因为我们对面向对象编程实在太熟悉了,以至于很多人对Go没有类型继承耿耿于怀。

严格意义讲,Go是面向接口编程的,因为Go没有对象概念,更没有类型继承。但Go接口的多态特性使其在设计高内聚低耦合的系统发挥更重要的作用。面向接口概念是面向对象的衍生,在多年的开发积累中,人们发现针对接口设计系统可以让系统扩展性和维护性更好。

在Go语言中,接口拥有举足轻重的地位,而面向接口编程也是Go语言核心的设计理念。接口是高度抽象的概念,它是一种类型可由type关键字声明,但它不像其他类型那样关注的是值,它关注的是行为(方法)。如果类型T的行为(实现的方法)和某个接口I声明的方法签名符合,那么类型T就实现了I的接口,在方法参数传递或各种类型校验中,T就是I的实现。

接口内部声明一个或多个方法签名,因此不能实例化,一般创建一个类型为接口的变量,它可以被赋值为任何满足该接口声明的实际类型的值,作为类型传递。

示例代码:

package main

import "fmt"

type Person struct {
	name string
}
type Student struct {
	Person         //Person类型匿名字段,结构体的嵌入形式,让Student可以直接引用Person的字段和方法,这种方式很像继承
	School string
}
type Worker struct {
	PS Person    //Person类型命名字段,结构体的聚合形式,必须通过Student.PS.Name形式引用Person的字段和方法
	Person      //要想Worker类型使用Person类型的接口,必须嵌入Person类型匿名字段
	Company string
}

type IPerson interface {
	Eat(string)
	Sleep() bool
}
type IStudent interface {
	IPerson
	Study()
}
type IWorker interface {
	IPerson
	Work()
}

func (p *Person) Sleep() bool {
	fmt.Println(p.name + "在睡觉,已经睡着了……")
	return true
}
func (p *Person) Eat(sth string) {
	fmt.Println(p.name + "正在吃" + sth)
}
func (s *Student) Study() {
	fmt.Println(s.name + "在" + s.School + "学习……")
}
func (w *Worker) Work() {
	fmt.Println(w.PS.name + "在" + w.Company + "工作……")
}

//向city切片中装载东东
func addPerson(city []interface{}, p interface{}) []interface{} {
	return append(city, p)
}
//[]interface{}作为入参,理论上可以接受任何类型的数据
func ShowCityPerson(city []interface{}){
	for _, person := range city {
		fmt.Println(person)
		//运行时动态判断数据类型
		if student, ok := person.(IStudent); ok {
			student.Study()
		}
		if worker, ok := person.(IWorker); ok {
			worker.Work()
		}
	}
}
func main() {
	var stu = new(Student)
	stu.name = "小明"
	stu.School = "光明小学"

	var wk = new(Worker)
	wk.PS.name = "老李"     //若结构体使用聚合方式
	wk.Company = "李氏商行"
	
	stu.Sleep()
	stu.Eat("早餐")
	stu.Study()
	wk.PS.Eat("午餐")
	wk.Work()
	wk.PS.Sleep()
	
	//定义空接口的切片,可以装入任意类型的东东
	var city []interface{}

	//向city切片中装入学生、工人
	city = addPerson(city, stu)
	city = addPerson(city, wk)

	//city可作为参数传递
	ShowCityPerson(city)
}

5.3 函数多返回值

在C,C++中,包括其他的一些高级语言是不支持多个函数返回值的。但是这项功能又确实是需要的,所以在C语言中一般通过将返回值定义成一个结构体,或者通过函数的参数引用的形式进行返回。而在Go语言中,一个函数可以没有返回值,也可以有一个返回值,也可以有返回多个值。
如下定义的一个函数,将两个入参交换后返回的:

package main
 
import "fmt"
 
func swap(x, y string) (string, string) {
   return y, x
}
 
func main() {
   a, b := swap("Mahesh", "Kumar")
   fmt.Println(a, b)
}

5.4 延后执行

Go有一个专门的延迟调用栈,使用defer 语句会将其后面跟随的语句压入栈中进行延迟处理,当前函数结束时(不管何种原因结束,即使是崩溃),会将延迟处理的语句按 defer 的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。
下面是一个示例代码:

package main
import (
    "fmt"
)
func main() {
    fmt.Println("defer begin")
    // 将defer后面跟随的语句放入延迟调用栈
    defer fmt.Println(1)
    defer fmt.Println(2)
    // 最后一个定义, 位于栈顶, 最先调用,所有defer的都在最后执行
    defer fmt.Println(3)
    fmt.Println("defer end")
}

代码执行输出如下:

defer begin
defer end
3
2
1

5.5 异常处理

Go不支持try…catch…finally这样的结构化的异常解决方式,因为觉得会增加代码量,且会被滥用,不管多小的异常都抛出。

通常情况下,我们希望:当发生错误后,可以捕获到错误,并进行处理,保证程序可以继续运行,还可以捕获到错误后给管理员一个提示(邮件,短信)。

Go内置error对象,使用defer…recover…panic机制来捕获处理。严重异常一般由golang内部自动抛出,不需要用户主动抛出,避免传统try…catch写得到处都是的情况。当然,用户也可以使用panic(‘xxxx’)主动抛出,只是这样就使这一套机制退化成结构化异常机制了。

下面defer…recover机制来捕获处理“除0”异常的例子:

package main

import (
    "fmt"
    "time"
)

func test() {
    //用defer + recover 来捕获和处理异常
    defer func() {
        err := recover() //recover内置函数可以捕获到异常
        if err != nil {  //nil是err的零值
            fmt.Println("err=", err)
            //这里可以把信息发送给管理员
            fmt.Println("发送信息给管理员admin@steven.com")
        }
    }() //匿名函数的调用方式一:func(){}()
    num1 := 10
    num2 := 0
    res := num1 / num2   //除0产生异常,会被defer延迟函数捕获
    fmt.Println("res=", res) 
}
func main() {
    //测试
    test()
    for {
        fmt.Println("main()...下面的代码。。。。")
        time.Sleep(time.Second)
    }
}

5.6 强大且高性能的网络编程

Go天生具备了去中心化、分布式等特性,提供了丰富便捷的网络编程接口,比如socket用net.Dial(基于tcp/udp,封装了传统的connect、listen、accept等接口)、http用http.Get/Post()、rpc用client.Call(‘class_name.method_name’, args, &reply)等等,在《Golang学习笔记(二):第一个可用于生产环境下的真正Go程序》中我们也看到了net/http包的强大之处。

5.7 内存管理及回收

从C到C++,从程序性能的角度来考虑,这两种语言允许程序员自己管理内存,包括内存的申请和释放等。因为没有垃圾回收机制所以C/C++运行起来速度很快,但是随着而来的是程序员对内存使用上的很谨小慎微的考虑。因为哪怕一点不小心就可能会导致“内存泄露”使得资源浪费或者“野指针”使得程序崩溃等,尽管C++11后来使用了智能指针的概念,但是程序员仍然需要很小心的使用。后来为了提高程序开发的速度以及程序的健壮性,java和C#等高级语言引入了GC机制,即程序员不需要再考虑内存的回收等,而是由语言特性提供垃圾回收器来回收内存。但是随之而来的可能是程序运行效率的降低。

Go也有类似的内存管理及回收机制,作为初学者先了解这点即可。

5.8 代码跨平台及交叉编译

Go开发代码可以不用任何修改直接在不同的平台下编译、执行,但需要注意两点:
1.有两种整数类型 int 和 uint,它们分别对应特定 CPU 平台的字长(机器字大小),实际开发中由于编译器和计算机硬件的不同,int 和 uint 所能表示的整数大小会在 32bit 或 64bit 之间变化。
2.要注意和平台密切相关的库,可能会在不同平台表现不同,比如time.Nanosecond()在windows和linux下获得的精度不一样。

同时Go提供了交叉编译功能,我们可以在运行 Linux 系统的计算机上开发运行 Windows 下运行的应用程序,反之亦然。

6 结语

这篇博文是我开始学习Go到目前为止(还没实操项目),所看过的关于Go语言介绍类文章的一个总结。

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值