初识Go
声明:内容来自《Go语言编程》
[TOC]
1 Go语言主要特性
- 自动垃圾回收
- 更丰富的内置类型
- 函数多返回值
- 错误处理
- 匿名函数和闭包
- 类型和接口
- 并发编程
- 反射
- 语言交互性
1.1 自动垃圾回收
内存泄露的最佳解决方案是在语言级别引入自动垃圾回收算法(Garbage Collection,简称GC)。
所谓垃圾回收,即所有的内存分配动作都会被在运行时记录,同时任何对 该内存的使用也都会被记录,然后垃圾回收器会对所有已经分配的内存进行跟踪监测,一旦发现 有些内存已经不再被任何人使用,就阶段性地回收这些没人用的内存。
使用Go语言,系统会自动帮我们判断,并在合适的时候(比如CPU 相对空闲的时候)进行自动垃圾收集工作。
1.2 更丰富的内置类型
- map (可以认为是字典)
- slice(一种可动态增长的数组)
1.3 函数多返回值
Go语言革命性地在静态开发语言阵营中率先提供了多返回值功能。
1.4 错误处理
Go语言引入了3个关键字用于标准的错误处理流程,这3个关键字分别为defer、panic和 recover。Go语言的错误处理机制可以大量减少代码量,让开发者也无需仅仅为了程序安全性而添加大量 一层套一层的try-catch语句。
1.5 匿名函数和闭包
在Go语言中,所有的函数也是值类型,可以作为参数传递。Go语言支持常规的匿名函数和闭包。
1.6 类型和接口
类型的定义使用struct关键字。引入了一个无比强大的“非侵入式” 接口的概念。
//定义一个鸟类型 type Bird struct { ... } //鸟类型的方法 func (b *Bird) Fly() { // 以鸟的方式飞行 } //定义一个IFly接口 type IFly interface { Fly() } //使用接口 func main() { var fly IFly = new(Bird) fly.Fly() }
可以看出,虽然Bird类型实现的时候,没有声明与接口IFly的关系,但接口和类型可以直接转换,甚至接口的定义都不用在类型定义之前,这种比较松散的对应关系可以大幅降低因为接口调整而导致的大量代码调整工作。
1.7 并发编程
引入了goroutine概念,使用消息传递来共享内存。Go语言让并发编程变得更加轻盈和安全。
通过在函数调用前使用关键字go,我们即可让该函数以goroutine方式执行。goroutine是一种比线程更加轻盈、更省资源的协程。。Go语言通过系统的线程来多路派遣这些函数的执行,使得每个用go关键字执行的函数可以运行成为一个单位协程。当一个协程阻塞的时候,调度器就会自动把其他协程安排到另外的线程中去执行,从而实现了程序无等待并行化运行。而且调度的开销非常小,一颗CPU调度的规模不下于每秒百万次,这使得我们能够创建大量的goroutine,从而可以很轻松地编写高并发程序,达到我们想要的目的。
使用CSP(通信顺序进程,Communicating Sequential Process)模型来作为goroutine间的推荐通信方式。(在CSP模型中,一个并发系统由若干并行运行的顺序进程组成,每个进程不 能对其他进程的变量赋值。进程之间只能通过一对通信原语实现协作。)Go语言用channel(通道) 这个概念来轻巧地实现了CSP模型,可以方便地进行跨goroutine的通信。
由于一个进程内创建的所有goroutine运行在同一个内存地址空间中,因此如果不同的 goroutine不得不去访问共享的内存变量,访问前应该先获取相应的读写锁。Go语言标准库中的 sync包提供了完备的读写锁功能
//由两个goroutine进行并行的累加计算,待这两个计算过程都完成后打印计算结果 package main import "fmt" func sum(values [] int, resultChan chan int) { sum := 0 for _, value := range values { sum += value } resultChan <- sum // 将计算结果发送到channel中 } func main() { values := [] int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} resultChan := make(chan int, 2) go sum(values[:len(values)/2], resultChan) go sum(values[len(values)/2:], resultChan) sum1, sum2 := <-resultChan, <-resultChan // 接收结果 fmt.Println("Result:", sum1, sum2, sum1 + sum2) }
1.8 反射
反射(reflection),通过反射,你可以获取对象类型的详细信息,并可动态操作对象。
反射最常见的使用场景是做对象的序列化(serialization,有时候也叫Marshal & Unmarshal)。
//以利用反射功能列出某个类型中所有成员变量的值 package main import ( "fmt" "reflect" ) type Bird struct { Name string LifeExpectance int } func (b *Bird) Fly() { fmt.Println("I am flying...") } func main() { sparrow := &Bird{"Sparrow", 3} s := reflect.ValueOf(sparrow).Elem() typeOfT := s.Type() for i := 0; i < s.NumField(); i++ { f := s.Field(i) fmt.Printf("%d: %s %s = %v\n", i,typeOfT.Field(i).Name,f.Type(), f.Interface()) } }
1.9 语言交互性
Cgo既是语言特性,同时也是一个工具的名称,用于重用现有C模块。在Go代码中,可以按Cgo的特定语法混合编写C语言代码,然后Cgo工具可以将这些混合的C 代码提取并生成对于C功能的调用包装代码。开发者基本上可以完全忽略这个Go语言和C语言的边界是如何跨越的。
2 hello world
package main import "fmt"// 我们需要使用fmt包中的Println()函数 func main() { fmt.Println("Hello, world. 你好,世界!") }
package : 每个Go源代码文件的开头都是一个package声明,表示该Go代码所属的包。包是Go语言里 最基本的分发单位,也是工程管理中依赖关系的体现。要生成Go可执行程序,必须建立一个名字为main的包,并且在该包中包含一个叫main()的函数(该函数是Go可执行程序的执行起点)。
main() : Go语言的main()函数不能带参数,也不能定义返回值。命令行传入的参数在os.Args变量 中保存。如果需要支持命令行开关,可使用flag包。
函数: 在函数返回时没有被明确赋值的返回值都会被设置为默认 值,比如result会被设为0.0,err会被设为nil。
func Compute(value1 int, value2 float64) (result float64, err error) { // 函数体 }
2.1 编译并运行
go run hello.go
使用这个命令,会将编译、链接和运行3个步骤合并为一步,运行完后在当前目录下也看不 到任何中间文件和最终的可执行文件。
go build hello.go
只生成编译结果而不自动运行。编译结果为hello的可执行文件。
3 工程管理
Go命令行工具的革命性之处在于彻底消除了工程文件的概念,完全用目录结构和包名来推 导工程结构和构建顺序。
示例:
这样一个程序:
$ calc help USAGE: calc command [arguments] ... The commands are: sqrt Square root of a non-negative value. add Addition of two values. $ calc sqrt 4 # 开根号 2 $ calc add 1 2 # 加法 3
工程结构:
一个正常的工程目录组织应该如下所示: <calcproj> ├─<src> ├─<calc> ├─calc.go ├─<simplemath> ├─add.go ├─add_test.go ├─sqrt.go ├─sqrt_test.go ├─<bin> ├─<pkg>#包将被安装到此处
- 可执行程序,名为calc,内部只包含一个calc.go文件;
- 算法库,名为simplemath,每个command对应于一个同名的go文件,比如add.go。
在上面的结构里,带尖括号的名字表示其为目录。xxx_test.go表示的是一个对于xxx.go的单元 测试,这也是Go工程里的命名规则。
//calc.go package main import "os" // 用于获得命令行参数os.Args import "fmt" import "simplemath" import "strconv" var Usage = func() { fmt.Println("USAGE: calc command [arguments] ...") fmt.Println("\nThe commands are:\n\tadd\tAddition of two values.\n\tsqrt\tSquar root of a non - negative value.") } func main() { args := os.Args if args == nil || len(args) < 2 { Usage() return } switch args[0] { case "add": if len(args) != 3 { fmt.Println("USAGE: calc add <integer1><integer2>") return } v1, err1 := strconv.Atoi(args[1]) v2, err2 := strconv.Atoi(args[2]) if err1 != nil || err2 != nil { fmt.Println("USAGE: calc add <integer1><integer2>") return } ret := simplemath.Add(v1, v2) fmt.Println("Result: ", ret) case "sqrt": if len(args) != 2 { fmt.Println("USAGE: calc sqrt <integer>") return } v, err := strconv.Atoi(args[1]) if err != nil { fmt.Println("USAGE: calc sqrt <integer>") return } ret := simplemath.Sqrt(v) fmt.Println("Result: ", ret) default: Usage() } }
// add.go package simplemath func Add(a int, b int) int { return a + b }
// add_test.go package simplemath import "testing" func TestAdd1(t *testing.T) { r := Add(1, 2) if r != 3 { t.Errorf("Add(1, 2) failed. Got %d, expected 3.", r) } }
// sqrt.go package simplemath import "math" func Sqrt(i int) int { v := math.Sqrt(float64(i)) return int(v) }
// sqrt_test.go package simplemath import "testing" func TestSqrt1(t *testing.T) { v := Sqrt(16) if v != 4 { t.Errorf("Sqrt(16) failed. Got %v, expected 4.", v) } }
将当前的工程的根目录添加到环境变量GOPATH中。
生成可执行文件。加入放到当前工程根目录的bin文件夹中:
cd /calcproj mkdir bin go build calc
我们不需要写makefile,因为这个工具会替我们分析,知道目标代码的编译结果应该是一个包还是一个可执行文件,并分析import语句以了解包的依赖关系,从而在编译calc.go之前先把依赖的simplemath编译打包好。
由于已经写了测试代码,并且已添加到环境变量GOPATH中,所以可以直接测试:
go test simplemath
若需要调试,可直接使用gdb调试编译后的可执行文件(gdb version >7.1),eg:
gdb calc
帮助GO初学者快速入门Golang