某些标记之后的换行符会被转换为分号,因此换行符的位置对于正确解析 Go 代码至关重要。例如,函数的左括号 { 必须与函数声明的结尾在同一行,否则会报错 unexpected semicolon or newline before {。而在表达式 x + y 中,可以在 + 运算符之后换行,但不能在之前换行。
1 2 3
funcmain() { fmt.Println("Hello, 世界") }
Command-Line Arguments
可以使用 os.Args 变量获取命令行参数,该变量是一个字符串切片。os.Args[0] 是命令本身,剩余元素是程序启动时用户传递的参数。使用 var 声明语句定义变量,变量可以在声明时进行初始化。如果未显式初始化,则隐式初始化为该类型的零值(zero value),数值类型为 0,字符串类型为空串 ""。
for 语句是 Go 中唯一的循环语句,可以充当其他语言中常规的 for、while 循环以及无限循环。Go 语言不允许未使用的局部变量,否则会报错 declared and not used。使用 += 在循环中拼接字符串的开销较大,每次都会生成新字符串,而旧字符串则不再使用等待 GC,可以使用 strings.Join 方法提升性能,一次性拼接所有字符串。
var palette = []color.Color{color.White, color.Black}
const ( whiteIndex = 0// first color in palette blackIndex = 1// next color in palette )
funcmain() { //!-main // The sequence of images is deterministic unless we seed // the pseudo-random number generator using the current time. // Thanks to Randall McPherson for pointing out the omission. rand.Seed(time.Now().UTC().UnixNano())
变量声明的通用形式为 var name type = expression。如果省略 type 则类型由表达式推断,如果省略 = expression 则必须显式指定类型,初始值为该类型的零值。数值类型为 0,字符串类型为空串 "",布尔类型为 false,接口和引用类型(切片、指针、哈希表、通道和函数)为 nil。像数组或者结构体聚合类型的元素或字段的零值就是自身的零值。
零值机制确保变量始终有其类型所定义的明确值,Go 语言中不存在未初始化变量的概念。可以同时声明一组变量,如果省略类型则可以同时声明不同类型的变量。包级变量会在 main 函数开始之前初始化,局部变量在声明时初始化。
1 2 3
var i, j, k int// int, int, int var b, f, s = true, 2.3, "four"// bool, float64, string var f, err = os.Open(name) // os.Open returns a file and an error
Short Variable Declarations
在函数中可以使用简短变量声明(short variable declaration)的形式声明和初始化局部变量,形式为 name := expression。简短变量声明常用于声明和初始化大多数局部变量,而 var 声明常用于变量类型和表达式类型不同、或者稍后赋值且初始值不重要的局部变量。
1 2 3
i := 100// an int var boiling float64 = 100// a float64 i, j = j, i // swap values of i and j
x := 1 p := &x // p, of type *int, points to x fmt.Println(*p) // "1" *p = 2// equivalent to x = 2 fmt.Println(x) // "2"
函数返回局部变量的地址是安全的,即使函数调用返回该局部变量 v 仍会存在。由编译器逃逸分析确定,该变量会在堆上分配。根据静态分析知识,为保证安全性,分析肯定是偏向误报(Sound)而不是漏报(Complete)。每次调用函数 f 返回的值都不同。每次获取变量的地址或者复制指针时,都会为该变量创建新的别名(aliases),*p 是 v 的别名。
1 2 3 4 5
var p = f() funcf() *int { v := 1 return &v }
1 2 3 4 5 6 7 8 9 10
var n = flag.Bool("n", false, "omit trailing newline") var sep = flag.String("s", " ", "separator")
另一种创建变量的方式是使用内置函数 new,表达式 new(T) 创建类型为 T 的未命名变量,将其初始化为类型 T 的零值,返回类型为 *T 的地址值。使用 new 创建的变量和普通局部变量没有区别,只是后者需要显式获取地址。
1 2 3 4 5 6 7 8
funcnewInt() *int { returnnew(int) }
funcnewInt() *int { var dummy int return &dummy }
通常每次调用 new 都会返回具有唯一地址的不同变量,例外情况是,如果两个变量的类型不携带任何信息且大小为零(例如 struct{} 或 [0]int),则根据实现的不同可能会具有相同的地址(实测得到的是不同地址)。由于 new 是内置函数而不是关键字,所以可以被重新定义为其他东西,不过此时不能在 delta 中使用内置的 new 函数。
1
funcdelta(old, newint)int { returnnew - old }
Lifetime of Variables
变量的声明周期是指其在程序执行过程中的存活时间。包级变量在整个程序执行过程中存活,局部变量在声明时创建,在不被引用时回收(GC 可达性分析)。因为变量的生命周期取决于可达性,所以局部变量在函数返回之后仍有可能存活。编译器会决定将变量分配到堆中还是栈中,这一决定并非取决于使用 var 还是 new 来声明变量(Pointers 小节中提到过的逃逸分析)。例如,下面示例中 x 必须在堆上分配,而 y 可以在栈上分配。
1 2 3 4 5 6 7 8 9 10 11 12
var global *int
funcf() { var x int x = 1 global = &x }
funcg() { y := new(int) *y = 1 }
Assignments
Go 语言中仅有后置 x++ 和 x-- 而没有前置写法,而且该操作被视为语句而不是表达式,所以不能将其赋值给变量或者参与运算。
类型声明形如 type name underlying-type,用于定义一个新的具有某个底层类型(underlying type)的命名类型(named type)。即使两个类型具有相同的底层类型,它们也是不同的类型,不能直接比较或组合,而需要使用 T(x) 进行显式类型转换。命名类型将底层类型的不同使用方式区分开来,避免不同使用方式之间混淆。
if x := f(); x == 0 { fmt.Println(x) } elseif y := g(x); x == y { fmt.Println(x, y) } else { fmt.Println(x, y) } fmt.Println(x, y) // compile error: x and y are not visible here
Basic Data Types
Go 语言的类型分为四类:基本类型、聚合类型、引用类型和接口类型。基本类型包括数值类型、字符串和布尔类型,聚合类型包括数组和结构体,引用类型包括指针、切片、哈希表、函数和通道。
var x complex128 = complex(1, 2) // 1+2i var y complex128 = complex(3, 4) // 3+4i fmt.Println(x*y) // "(-5+10i)" fmt.Println(real(x*y)) // "-5" fmt.Println(imag(x*y)) // "10"
Strings
字符串是不可变的字节序列,内置函数 len 返回字符串的字节数量,而不是字符数量,索引操作 s[i] 获取字符串 s 的第 i 个字节。字符串的第 i 个字节不一定就是第 i 个字符,因为非 ASCII 码点的 UTF-8 编码需要多个字节。使用 s[i:j] 可以获取子字符串,该子字符串是一个新的字符串,不过和原串共享底层字节数组。由于可以共享底层内存,所以字符串的复制和子串操作的开销很低。
1 2 3
s := "hello, world" fmt.Println(len(s)) // "12" fmt.Println(s[0], s[7]) // "104 119" ('h' and 'w')
// "program" in Japanese katakana s := "プログラム" fmt.Printf("% x\n", s) // "e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0" r := []rune(s) fmt.Printf("%x\n", r) // "[30d7 30ed 30b0 30e9 30e0]"
Strings and Byte Slices
字符串 s 可以使用 []byte(s) 转换为字节切片,然后使用 string(b) 转换回来。两个操作通常都会进行复制操作,以确保 b 的可变性和 s2 的不可变性。bytes 包提供 Buffer 类型,类似 Java 中的 StringBuilder,该类型无需初始化,其零值可以直接使用,因为之后调用的方法中会判断底层切片 buf 是否为 nil,然后为其分配内存。
1 2 3
s := "abc" b := []byte(s) s2 := string(b)
Conversions between Strings and Numbers
1 2 3 4 5 6
x := 123 y := fmt.Sprintf("%d", x) fmt.Println(y, strconv.Itoa(x)) // "123 123"
x, err := strconv.Atoi("123") // x is an int y, err := strconv.ParseInt("123", 10, 64) // base 10, up to 64 bits
Constants
const 常量的底层类型必须是基本类型(和 Java 中的 final 很不一样)。当常量作为一个组声明时,除该组的第一个元素外,剩余元素的右侧表达式可以省略,此时默认会使用之前元素的表达式。
1 2 3 4 5 6 7
const ( a = 1 b c = 2 d ) fmt.Println(a, b, c, d) // "1 1 2 2"
The Constant Generator iota
可以使用常量生成器 iota 创建枚举常量组,iota 的值从 0 开始,每次递增 1。
1 2 3 4 5 6 7 8
type Flags uint const ( FlagUp Flags = 1 << iota// is up FlagBroadcast // supports broadcast access capability FlagLoopback // is a loopback interface FlagPointToPoint // belongs to a point-to-point link FlagMulticast // supports multicast access capability )
var x float32 = math.Pi var y float64 = math.Pi var z complex128 = math.Pi
const Pi64 float64 = math.Pi var x float32 = float32(Pi64) var y float64 = Pi64 var z complex128 = complex128(Pi64)
只有常量可以不指定类型,当将无类型常量赋值给变量时,该常量会隐式转换为变量的类型。
1 2 3 4
var f float64 = 3 + 0i// untyped complex -> float64 f = 2// untyped integer -> float64 f = 1e123// untyped floating-point -> float64 f = 'a'// untyped rune -> float64
无论隐式还是显式转换,在转换时目标类型必须能够表示原始值,对于实数和复数的浮点数,允许四舍五入。
1 2 3 4 5 6 7 8 9
const ( deadbeef = 0xdeadbeef// untyped int with value 3735928559 a = uint32(deadbeef) // uint32 with value 3735928559 b = float32(deadbeef) // float32 with value 3735928576 (rounded up) c = float64(deadbeef) // float64 with value 3735928559 (exact) d = int32(deadbeef) // compile error: constant overflows int32 e = float64(1e309) // compile error: constant overflows float64 f = uint(-1) // compile error: constant underflows uint )
在未显式指定类型的变量声明中,无类型常量的特性会决定变量的默认类型。无类型整型默认转换为 int 类型,无类型浮点数和复数默认转换为 float64 和 complex128。如果要使用其他类型,需要显示类型转换,或者在变量声明中指定类型。在将无类型常量转换为接口值时,默认值非常重要,因为它们会决定接口的动态类型。
1 2 3 4
i := 0// untyped integer; implicit int(0) r := '\000'// untyped rune; implicit rune('\000') f := 0.0// untyped floating-point; implicit float64(0.0) c := 0i// untyped complex; implicit complex128(0i)
fmt.Println(summer[:20]) // panic: out of range endlessSummer := summer[:5] // extend a slice (within capacity) fmt.Println(endlessSummer) // "[June July August September October]"
funcappendInt(x []int, y int) []int { var z []int zlen := len(x) + 1 if zlen <= cap(x) { // There is room to grow. Extend the slice. z = x[:zlen] } else { // There is insufficient space. Allocate a new array. // Grow by doubling, for amortized linear complexity. zcap := zlen if zcap < 2*len(x) { zcap = 2 * len(x) } z = make([]int, zlen, zcap) copy(z, x) // a built-in function; see text } z[len(x)] = y return z }
_ = &ages["bob"] // compile error: cannot take address of map element
可以使用基于范围的 for 循环遍历哈希表。map 的迭代顺序不是确定性的,实际实现为随机顺序,如果要按照顺序遍历,需要对键进行排序。由于已知 names 的最终大小,所以预分配指定容量的数组会更高效。
1 2 3 4 5 6 7 8
names := make([]string, 0, len(ages)) for name := range ages { names = append(names, name) } sort.Strings(names) for _, name := range names { fmt.Printf("%s\t%d\n", name, ages[name]) }
funcadd(x int, y int)int { return x + y } funcsub(x, y int) (z int) { z = x - y; return } funcfirst(x int, _ int)int { return x } funczero(int, int)int { return0 }
f = product // compile error: can't assign f(int, int) int to f(int) int
Anonymous Functions
命名函数只能在包级进行声明,而匿名函数可以在任意表达式中使用。每次调用 squares 都会创建一个局部变量 x,并返回一个类型为 func() int 的匿名函数。此时,匿名函数可以访问和修改外层函数的局部变量 x。这些隐含的变量就是将函数视为引用类型,以及函数值之间无法比较的原因。这类函数值使用闭包(closures)技术实现。就像之前提到的返回局部变量的地址的例子,闭包示例也说明变量的生命周期不是由作用域决定,而是由可达性决定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// squares returns a function that returns // the next square number each time it is called. funcsquares()func()int { var x int returnfunc()int { x++ return x * x } }
functriple(x int) (result int) { deferfunc() { result += x }() return double(x) }
fmt.Println(triple(4)) // "12"
Panic
当 Go 运行时检测到严重的错误时,会产生 panic,正常执行会停止,从函数栈顶到 main 函数的所有延迟函数调用会依次执行,然后程序会崩溃并记录日志。可以显示调用内置的 painc 函数。对于函数的前置条件进行确认是良好的做法,但是除非能够提供更详细的错误信息或更早地检测到错误,否则没有必要去确认在运行时会自动检查的条件。
1 2 3 4 5 6
funcReset(x *Buffer) { if x == nil { panic("x is nil") // unnecessary! } x.elements = nil }
funcParse(input string) (s *Syntax, err error) { deferfunc() { if p := recover(); p != nil { err = fmt.Errorf("internal error: %v", p) } }() // ...parser... }
Methods
Method Declarations
方法声明就是在函数名之前添加额外的参数,该参数将函数和该参数的类型绑定。额外的参数 p 被称作该方法的接收者(receiver),在 Go 语言中,没有使用 this 或 self 表示接收者,而是像对待普通参数一样选择接收者的名称,通常使用类型的首字母作为其名称。以下两个函数声明不会相互冲突,一个是包级函数,另一个是 Point 类型的方法。
在 Go 语言中,字段和方法不能同名(和 Java 不同)。和其他面向对象语言不同,Go 中可以为大多数类型定义方法,而不仅仅是结构体类型。例外情况是:① 不能直接为基本类型定义方法,而必须使用 type 创建命名类型;② 不能为指针和接口类型定义方法,但是接收者可以是指向非指针类型的指针。另外,类型和方法必须定义在相同的包中。
1 2 3 4 5 6 7 8 9 10 11
type Point struct{ X, Y float64 }
// traditional function funcDistance(p, q Point)float64 { return math.Hypot(q.X-p.X, q.Y-p.Y) }
// same thing, but as a method of the Point type func(p Point) Distance(q Point) float64 { return math.Hypot(q.X-p.X, q.Y-p.Y) }
funcmain() { var buf *bytes.Buffer if debug { buf = new(bytes.Buffer) // enable collection of output } f(buf) // NOTE: subtly incorrect! if debug { // ...use buf... } }
// If out is non-nil, output will be written to it. funcf(out io.Writer) { // ...do something... if out != nil { out.Write([]byte("done!\n")) // panic: nil pointer dereference } }
Type Assertions
类型断言(type assertion)是对接口值执行的操作,形如 x.(T),其中 x 是接口类型的表达式,T 是断言类型。如果 T 是具体类型,那么类型断言会检查 x 的动态类型是否和 T 相同,如果相同则返回 x 的动态值,否则引发 panic。
1 2 3 4
var w io.Writer w = os.Stdout f := w.(*os.File) // success: f == os.Stdout c := w.(*bytes.Buffer) // panic: interface holds *os.File, not *bytes.Buffer
如果 T 是接口类型,那么类型断言会检查 x 的动态类型是否满足 T,如果满足则返回类型为 T 的接口值,该接口值和 x 具有相同的动态类型和动态值。不论 T 是什么类型,如果 x 是 nil,则断言会失败。
1 2 3 4 5
var w io.Writer w = os.Stdout rw := w.(io.ReadWriter) // success: *os.File has both Read and Write w = new(ByteCounter) rw = w.(io.ReadWriter) // panic: *ByteCounter has no Read method
如果使用以下类型断言方式,则断言失败不会引发 panic,而是额外返回 false。
1 2 3
var w io.Writer = os.Stdout f, ok := w.(*os.File) // success: ok, f == os.Stdout b, ok := w.(*bytes.Buffer) // failure: !ok, b == nil
Type Switches
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
funcsqlQuote(x interface{})string { switch x := x.(type) { casenil: return"NULL" caseint, uint: return fmt.Sprintf("%d", x) // x has type interface{} here. casebool: if x { return"TRUE" } return"FALSE" casestring: return sqlQuoteString(x) // (not shown) default: panic(fmt.Sprintf("unexpected type %T: %v", x, x)) } }
Rust 代码中的变量和函数名使用 snake case 风格,使用下划线分隔单词。Rust 是基于表达式的语言(expression-based),函数体由一系列语句(statement)和可选地结尾表达式(expression)组成,语句不会返回值而表达式会。表达式结尾没有分号,如果加上分号它就变为语句。如果函数没有返回值,则可以省略返回类型,会隐式地返回空元组,否则需要使用 -> 显式声明。代码块 {}和 if 都是表达式,可以在 let 语句右侧使用。
1 2 3
fnfive() ->i32 { 5 }
1 2 3
letcondition = true; letnumber = if condition { 5 } else { 6 }; println!("The value of number is: {number}");
{ lets = String::from("hello"); // s is valid from this point forward // do stuff with s } // this scope is now over, and s is no longer valid
所有权(Ownership)机制让 Rust 无需 GC 就能保证内存安全。GC 是运行时根据可达性分析回收内存,而所有权是在编译时确定变量的作用域,当内存的所有者变量离开作用域之后,相应内存就可以被释放。当变量离开作用域时,Rust 会自动调用 drop 函数来释放内存,类似 C++ 的 RAII 机制。
字符串 String 的数据存储在堆上,字符串变量包含指向堆中数据的指针、数据的长度和容量,其存储在栈上。使用 let s2 = s1; 只会复制引用(浅拷贝),为避免 s1 和 s2 离开作用域之后,释放相同内存两次(调用 drop 函数),该语句执行之后 Rust 会使 s1 无效,不需要在其离开作用域之后回收内存,该操作被称为移动(move)。
1 2 3
lets1 = String::from("hello"); lets2 = s1; println!("{s1}, world!"); // error[E0382]: borrow of moved value: `s1`
当给已有值的变量赋新值时,Rust 会立即调用 drop 释放原始值的内存。如果想要执行深拷贝,可以使用 clone 函数。如果类型实现 Copy 特征,则旧变量在被赋值给其他变量之后仍然有效。不能为实现 Drop 特征的类型实现 Copy 特征。
1 2 3
letmut s = String::from("hello"); s = String::from("ahoy"); println!("{s}, world!");
References and Borrowing
引用(reference)类似指针,它是一个地址,但是和指针不同,Rust 保证引用指向某个特定类型的有效值。传递引用值不会转移所有权,所以可以在 main 中继续使用 s1。函数 calculate_length 中的局部变量 s 是引用类型,其不拥有值的所有权,所以离开作用域之后,其指向的值也不会被回收,所以创建引用的行为被称为借用(borrowing)。
1 2 3 4 5 6 7 8 9
fnmain() { lets1 = String::from("hello"); letlen = calculate_length(&s1); println!("The length of '{s1}' is {len}."); }
if state.existed_in(1900) { Some(format!("{state:?} is pretty old, for America!")) } else { Some(format!("{state:?} is relatively new.")) } }
Common Collections
Storing Lists of Values with Vectors
1 2 3 4 5 6 7 8 9 10
letv = vec![1, 2, 3, 4, 5];
letthird: &i32 = &v[2]; println!("The third element is {third}");
letthird: Option<&i32> = v.get(2); match third { Some(third) => println!("The third element is {third}"), None => println!("There is no third element."), }
1 2 3 4
letmut v = vec![100, 32, 57]; foriin &mut v { *i += 50; }
Rust 中每个引用都有生命周期(lifetime),通常生命周期可以被隐式地推断出来,否则需要显式写出。生命周期的主要目标是避免悬垂引用,Rust 编译器使用借用检查器(borrow checker)比较作用域,确保所有的借用都是有效的。r 和 x 的生命周期分别被标记为 'a 和 'b,编译器会发现 r 引用的变量 x 的生命周期比自身更小,这会导致悬垂引用,所以编译器会报错。
1 2 3 4 5 6 7 8 9 10
fnmain() { // error[E0597]: `x` does not live long enough letr; // ---------+-- 'a // | { // | letx = 5; // -+-- 'b | r = &x; // | | } // -+ | // | println!("r: {r}"); // | } // ---------+
1 2 3
&i32// a reference &'ai32// a reference with an explicit lifetime &'amuti32// a mutable reference with an explicit lifetime
生命周期注解语法用于将函数参数和返回值的生命周期相关联,生命周期也是泛型。以下函数签名表示,泛型生命周期参数 'a 的具体生命周期是 x 和 y 中生命周期的较小者,返回值不能在生命周期 'a 结束之后使用。由于下面的 result 在 y 生命周期结束之后访问,所以会引发编译错误。
1 2 3
fnlongest<'a>(x: &'astr, y: &'astr) -> &'astr { if x.len() > y.len() { x } else { y } }
1 2 3 4 5 6 7 8 9
fnmain() { letstring1 = String::from("long string is long"); letresult; { letstring2 = String::from("xyz"); result = longest(string1.as_str(), string2.as_str()); // error[E0597]: `string2` does not live long enough } println!("The longest string is {result}"); }
结构体可以存储引用类型,不过此时需要使用生命周期注解。下面注解意味着,ImportantExcerpt 实例不能在其字段 part 的生命周期结束之后使用。