这是针对 C++ 复杂的 3L(Load, Link, Library) 机制的一个重大修正。在实际开发中,我也比较喜欢把所有的依赖模块都先打包成静态库,然后最终静态链接成一个“几乎没有任何依赖的”可执行文件。而不喜欢通过动态链接依赖操作系统中安装的各种库,因为环境变化太多很容易出问题。
声明关键字 + 名字 + 声明类型
var number int // 声明变量 + 名字 number + 整数类型func SomeFun() int { ... } // 声明函数 + 名字 SomeFun + 返回值类型 + 函数内容type SomeStruct struct { ... } // 声明类型 + 名字 SomeStruct + 结构体类型 + 结构体内容
,
进行多个值返回func main() { var min, max int // 调用 min, max = MinMax(78, 65) fmt.Printf("Minmium is: %d, Maximum is: %d\n", min, max)}
// 声明返回值变量为 min, max,直接运行过程中对其赋值即可func MinMax(a int, b int) (min int, max int) { if a < b { min = a max = b } else { // a = b or a > b min = b max = a } return}
// 没有声明返回值的变量名,通过逗号来 return 多个值func ReturnTwo() (int, int) { return 2, 3}
=
而声明加赋值为 :=
var s = "go" // 用 "go" 作为值,定义了变量 s 是字符串类型j := 9 // 直接用 9 作为值,定义了变量 j 是数字类型,节省了 var 这个写法
5. Go 语言的类型系统和 C++ 很像,也有基本类型和派生类型,其中派生类型可以组合多个基本类型来构成。Go 的派生类型有:
指针 *type数组 [...]type集合 map[type]type切片 []typeChannel chan type结构体 type XXX struct {...}函数 type XXX func(xxx) xxx {...}接口 type XXX interface {...}Go 语言的中括号 [] 用在了数组、切片、集合三种类型上,比 C++ 仅仅用于数组丰富的多。
无符号 | 有符号 | 浮点 | 数字类型 |
---|---|---|---|
uint8 | int8 | float32 | byte |
uint16 | int16 | float64 | rune |
uint32 | int32 | complex64 | uint |
uint64 | int64 | complex128 | uintptr |
value, ok := m[key] //返回值 2 表示是否存在if (ok) { ...}
const( a = iota //0 b //1 c //2)
for
循环中,遍历容器用 range
for i, v := range arr { fmt.Printf("arr[%d]: %v", i, v)}
// 为回调函数定义一个类型,方便作为参数类型type cb func(int) int
// 输入回调函数作为参数的函数func testCallBack(x int, f cb) { f(x)}
// 具体的回调函数func callBack(x int) int { return x}
func main() { // 使用回调 testCallBack(1, callBack)}
&
*
.
操作符读取指针指向对象的成员,而不需要 ->
操作符Go 语言全是值传递,所以必须要有指针类型,否则所有变量都必须要复制值,太浪费性能了。
Go 语言通过自动检测“逃逸”来自动决定是否分配在堆上,这样连 new 这种关键字也不需要了,也无需好像 Java 语言一样区分在基本数据类型(在栈上)和对象数据类型(在堆上),所以也无所谓装箱拆箱。也不需要类似 C# 的 struct 类型(值传递的栈上结构)
func test_ptr() *int { var i = 10 return &i // go 很聪明的发现了这个情况}
func main() { var ret *int ret = test_ptr()
fmt.Printf("ret(%x): %v\n", ret, *ret) //输出:ret(c0000ba000): 10}
type Con struct { ptr *int}
func test(in *Con) { i := 110 in.ptr = &i // 局部变量的值被赋值到函数外了}
func main() { var con Con test(&con) fmt.Printf("result: %d in %#v\n", *con.ptr, con) // 可以输出 110 这个值
甚至可以把局部变量的指针通过输出参数(指针的指针)传出函数,这样局部变量也可以被放到堆上
func test(in **int) { // 输出参数,一个指针的指针,用来返回一个对象的地址 i := 110 *in = &i}
func main() { var result *int // 这个指针对象仅仅用来存放地址 test(&result) // 从 test() 函数中输出可用的对象地址 fmt.Printf("result: %d (%x)\n", *result, result) // 打印出 110 这个值}
var variable_name [SIZE] variable_type
balance := [...]float32{1000.0,2.0,3.4.7.2,50.8} // 初始化数组同时定义长度
arr []int
,如果下标访问越界,会抛出错误:panic: runtime error: index out of range
append
(s1, s2...)
把切片 s2 中的所有元素都添加到 s1 去,对切片扩容主要靠这个手段。
var slice1 []int = make([]int, 3, 5) //用 make() 来构建一个切片,len=3, cap=5
s := [] int {1,2,3} //直接初始化切片, [] 表示是切片,cap=len=3arr:=[...] int {1,2,3} // 注意 [...] 表示是数组,而不是切片
//初始化切片 s,是数组 arr 的引用,用 startIndex/endIndex 进行范围指定s := arr[startIndex:endIndex]
make([]int, len, cap) 返回构造的切片(也可以构造 map)
len
(s)
返回长度cap
(s)
返回容量append
(s, x, y, z)
追加 x, y, z 到 s 中去,可以增加 s 的容量 len 或者 capcopy
(s1, s2)
把 s2 的内容拷贝到 s1,控制一个切片中的数据主要靠这个手段,类似于 memcpy(),注意 copy() 并不会扩容,目标切片放不下的数据,会直接丢弃new
()
函数返回一个变量的指针,同时也分配这个变量的内存,这个变量的值会全部初始化为 0 或者 nil。这个操作可以视为先声明一个变量,然后再取此变量的指针作为返回值。但是对于符合类型来说,如 slice/map/channel 这些,new
()
就无法正确的给予初始化,所以需要 make
()
来进行构建。虽然 make()/new() 看起来很像 C++ 的 new 的作用,但实际并不一定会在堆上生成对象。Go 会自己进行“逃逸分析”来决定,所以好像用不用这两个函数都无所得的——如果不小心在不需要的地方被视为“逃逸”会影响内存分配的性能。
// circle.h class Circle { float radius; float getArea(); // golang 不需要专门声明方法}
// circle.cpp#include "circle.h"float Circle::getArea() { // golang 只需要定义具体的方法即可 return 3.14 * radius * radius;}
// 定义“类”type Circle struct { radius float64}
// 定义方法,c 变量就是 thisfunc (c Circle) getArea() float64 { return 3.14 * c.radius * c.radius}
func main() { var c1 Circle // 声明对象 c1.radius = 10.00 // 初始化对象 fmt.Println("面积 = ", c1.getArea()) // 调用对象的方法}
// 对象变量声明是指针 *Circle func (c *Circle) changeRadius(radius float64) { c.radius = radius}
这么一来,代码规范中的 Camel 方式和 PASCAL 方式的整理不存在了,首字母带了功能,不是随便能改的了。
很多 Go 程序的私有成员变量,都用 _
开头,这和 google 的代码规范有一定的关系。C++ 的 google 代码规范规定:私有成员变量以下划线 _
结尾。
type Pet struct { name string}type Dog struct { Pet // 继承 Pet 类型 Breed string}
// Pet 的 Speck() 方法func (p *Pet) Speak() string { return fmt.Sprintf("my name is %v", p.name)}
// Pet 的 Name() 方法func (p *Pet) Name() string { return p.name}
// Dog 的 Speak() 方法func (d *Dog) Speak() string { return fmt.Sprintf("%v and I am a %v", d.Pet.Speak(), d.Breed)}func main() { d := Dog{Pet: Pet{name: "spot"}, Breed: "pointer"} fmt.Println(d.Name()) // 调用基类 Pet 的 Name() fmt.Println(d.Speak()) // 调用子类 Dog 的 Speak()}
type Phone interface { call()}
type NokiaPhone struct {}func (p *NokiaPhone) call() { fmt.Println("I'm Nokia, I can call you!")}
func main() { var phone Phone // 这是一个指针类型 phone = new(NokiaPhone) // new() 返回的是 *NokiaPhone phone.call()}
func dosomething() (int, error) { return 1, nil}
func main() { value, err := dosomething(); // 立即处理,立即处理,立即处理,重要事情说三遍 if err != nil { // handle return } fmt.println(value);}
errors.New()
可以返回一个 error 类型的对象,注意这是一个接口(指针),这样可以让 ==
比较操作正确运行。error 对象可以很好的代替 C++ 的错误码。C++ 中为了定义错误码和打印错误字符串,往往需要同时维护一个数字宏和字符串宏,需要用某种特殊的宏写法才能实现。go 语言则天然每个错误(码)都自带输出字符串。但是如果随意的 return errors.New("...")
,容易造成大量的“没有名字”的错误对象,处理起来并不方便。
var ( // 类似定义错误码,可以全局使用 ZJTenantCodeMissing = errors.New("zhijian: missing tenant code."); ZJUserIDMissing = errors.New("zhijian: missing user id.");)
type User struct { UserID string UserName string}
func getUser(UserID string) (User, error) { if UserID == '' { return ZJUserIDMissing // 返回错误(码) } // coding...}
func main() { user, err := getUser('123') if err == ZJUserIDMissing { // 检查错误(码) // 处理user id missing }}
errors.New()
能返回的是对象而不带更多的信息,所以也有定义一个新的错误类型,只要实现了 error
接口就可以了。判断的时候不用 ==
,而是用 err.type
的类型进行判断。这个做法比纯“错误码”的用法能带来更多的错误处理细节。recover
()
获得异常对象,然后进行处理defer
一个异常处理函数panic
()
函数抛出 error
,调用后 panic
()
后面的代码将不再执行func error_process() { err := recover() if err != nil { // catch(err) {...} fmt.Println("catch:", err) }
//finally {...} fmt.Println("finally...")}
func main() { defer error_process() // try{...} panic("My panic error") // throw fmt.Println("After panic") // it would not be run}
go
启动一个 goroutine,概念上类似线程chan
作为符合类型关键字,用 <-
作为读写操作符func fibonacci(n int, c chan int) { x, y := 0, 1 for i := 0; i < n; i++ { fmt.Println("start ch <-", x) c <- x // 如果管道没空间了,会阻塞在这里 fmt.Println("end ch <-", x) x, y = y, x+y } close(c)}
func main() { var c chan int c = make(chan int, 1) // 管道中 2 个空间 go fibonacci(10, c) for i := range c { // 管道为空,就会阻塞在此 fmt.Println(i, "<- ch") }}
/* 输出结果start ch <- 0 end ch <- 0 start ch <- 1 end ch <- 1 start ch <- 1 0 <- ch 1 <- ch 1 <- ch end ch <- 1 start ch <- 2 end ch <- 2 start ch <- 3 end ch <- 3 start ch <- 5 2 <- ch 3 <- ch 5 <- ch end ch <- 5 start ch <- 8 end ch <- 8 start ch <- 13 end ch <- 13 start ch <- 21 8 <- ch 13 <- ch 21 <- ch end ch <- 21 start ch <- 34 end ch <- 34 34 <- ch */
import "reflect"
reflect.TypeOf()/reflect.ValueOf()
可以返回一个对象的类型和值reflect.methodByName()/reflect.FieldByName()
用一个字符串返回方法,成员Field.Tag
可以获得注解数值字符串`key1:"value1", key2:"value2"`
,还可以通过 Get("key1") 的方法获得 value1 的内容interface{}
是一种特殊的类型,任何的对象都可以转化这个类型的变量,类似 C++ 中的 void*
,在反射代码中非常常见,用于存放未知类型的变量