把 Hexo 换成了基于 Go 的 Hugo,所以有必要学一下 Golang 了(
之前也一直在想学个新的语言,一直在纠结选什么好,刚巧就是你了。
Table of Content
- 0x01 包
- 0x02 函数
- 0x03 返回值
- 0x04 变量
- 0x05 变量类型
- 0x06 类型转换
- 0x07 常量
- 0x08 循环
- 0x09 判断
- 0x10 defer
- 0x11 指针
- 0x12 结构体
- 0x13 数组
- 0x14 切片
- 0x15 make
- 0x16 range 遍历
- 0x17 映射
- 0x18 函数值和函数的闭包
0x01 包
Go 程序类似 Java,由包构成,并且程序总是从 main 包开始运行。包名与导入路径的最后一个元素相同。
package main // 声明当前文件所属的包,不需要引号
import "fmt" // 导入包,需要引号
import (
"os"
"math/rand"
) // 这样也可以
导入却未被使用的包编译器会报错。
Go 中如果一个函数/方法/变量/常量名字以大写字母开头,表示它已经被导出。如:
package main
import "fmt"
import "math"
func main() {
fmt.Println(math.Pi) // 3.141592653589793
// fmt.Println(math.pi) unexported, return undefined
}
再如上述代码中,Println
是 fmt
包导出的一个方法。
0x02 函数
定义函数格式如下:
func [functionName] ([param1 paramType], [param2 paramType]...) [returnValType] {
// ...
return // ...
}
与 C/C++/Java 的区别在于变量的类型在变量名之后而不是之前。另外,若有多个参数的类型相同,则可以简写:
func add(x int, y int) int {
return x + y
}
func dec(x, y int) int {
return x - y
}
0x03 返回值
Go 语言支持一个函数中返回多个值:
package main
import "fmt"
func swap(x, y: string) (string, string) {
return y, x
}
func main() {
a, b := swap("hello", "world")
fmt.Println(a, b) // world hello
}
还有“返回值命名”这种操作。直接在声明返回值的时候写下返回值的变量名:
func split(sum int) (x, y int) {
x = sum * 4 / 9 // 不需要再定义了
y = sum - x
return // 直接返回 x,y
}
这样可以一定程度上省掉注释写文档(
:=
运算符相当于定义变量后直接赋值:
var a int
a = 233
// just the same as..
b := 233
0x04 变量
上文提到用 var [variableName1], [variableName2] [variableType]
声明一个变量.若想在声明变量时初始化值,可以这样:
var a bool = false
var x, y int = 1, 2
t := "naive" // 如果有初始值,Go 可以进行类型推断
fmt.Println(a, x, y, t)
声明完不用的变量同样会使编译器报错。需要注意 :=
不能在函数作用于外部使用。如:
package main
import "fmt"
a := 233 // do not declare a variable like this
func main() {
// ...
}
0x05 变量类型
Go 的基本类型……
bool // 布尔
string // 字符串
// 整型,其中 int, uint, uintptr 的位宽由操作系统位数决定
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr // unsigned
byte // uint8 的别名
rune // int32 的别名, 表示一个 Unicode 码点
float32 float64 // 浮点,Go 没有 double
complex64 complex128 // 复数
package main
import (
"fmt"
"math/cmplx"
)
// 定义多个变量也可以因式分解关键字
var (
maxInt uint64 = 1 << 64 - 1 // 2^64-1
z complex128 = cmplx.Sqrt(-5 + 12i)
)
func main() {
fmt.Println("Type: %t Value: %v\n", maxInt, maxInt)
fmt.Println("Type: %t Value: %v\n", z, z)
}
Println中的占位符:
%t表示变量类型,
%v表示变量值. 当然你可以用
fmt.Printf()` 然后写那些格式占位符。
如果没有对已声明的变量赋值,那么他们的默认值为 0/false/空字符串。
0x06 类型转换
可以用 var a type = type(b)
格式。
var i int = 233
var f float64 = float64(i)
u := uint(f)
与 C 不同的是,Go 在不同类型的项之间赋值时需要显式转换。
0x07 常量
声明常量在其前面加上关键字 const
, 注意常量不能用 :=
语法声明:
const a = 2333
const Pi float64 = 3.14
const World = "世界"
const fake bool = false
0x08 循环
Go 只有 for 循环的结构……虽然 for 可以替代 while 但是有时候还是写 while 比较方便的呢。与 C/C++/JS 等语言的区别在于省略了把初始化语句、条件表达式、后置语句括起来的括号,但是仍然保留大括号。
package main
import "fmt"
func main() {
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
fmt.Println(sum)
}
如果去掉初始化和后置语句就变成类似 while 了:
for ; condition; {
}
其实……C 的 while 就是 Go 中的 for( 所以当初始化语句和后置语句没有的时候你甚至可以省略分号,这样 for 就成了 while 了:
package main
import "fmt"
func main() {
sum := 1
for sum < 1000 {
sum += sum
}
fmt.Println(sum)
}
省略循环条件下的无限循环:
for {
// do something infinity
}
循环的控制语句和其他语言类似,使用 break
语句退出循环结构,使用 continue
语句继续下一个循环。
0x09 判断
if else 结构
Go 的 if 语句与 for 类似,条件表达式外不需要小括号,而逻辑部分需要大括号。
func sqrt(x float64) string {
if x < 0 {
return sqrt(-x) + "i"
} else {
return fmt.Sprint(math.Sqrt(x))
}
}
区别于其他语言,Go 允许你在执行 if 的判断前进行初始化,和 for 的初始化语句类似
func pow(x, n, lim float64) float64 {
if v := math.Pow(x, n); v < lim {
return v
}
return lim // 注意,初始化语句的作用域仅限于大括号内,这里如果访问 v 就是 undefined
}
switch 结构
与其他语言类似并继承Go 特色,条件部分不需要小括号,可以有初始化语句。但还有一点与其它语言的不同在于 Go 的 switch 中 case 结束后不需要 break,Go 会自动帮你添加而不会运行选定 case 之后的所有 case(除非你指定了 fallthrough).
Go 的 case 不需要是常量,取值不用是整数。
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Print("Go runs on ")
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.", os)
}
}
0x10 defer
defer 语句将函数推迟到外层函数返回后进行,如:
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
程序先输出 hello, 等待 main 函数执行完返回后输出 world. 注意 defer 推迟的函数是压入栈(后进先出)中的,也就是说:
func main() {
defer fmt.Println("world")
defer fmt.Println("happy")
fmt.Println("hello")
}
以上程序的输出顺序是 hello happy world 而不是 hello world happy.
0x11 指针
定义一个指向 int 类型变量的指针:var p *int
,定义方法和 C/C++ 类似。
同样地,&
和 *
运算符的效果也是相同的。但是 Go 里没有指针运算(啥玩意啊.jpg)
& 操作符会生成一个指向其操作数的指针。
i := 233
p = &i
操作符表示指针指向的底层值。
fmt.Println(*p) // 通过指针 p 读取 i *p = 666 // 通过指针 p 设置 i fmt.Println(i) // 666
0x12 结构体
结构体定义格式如下:
type StructName struct {
member1 type1
member2 type2
// ...
}
定义结构体及其类型的变量示意:
package main
import "fmt"
type Vertex struct {
X int
Y int
}
func main() {
t := Vertex{2, 3}
fmt.Println(Vertex{1, 2})
fmt.Println(t)
}
与 C/C++ 类似,结构体成员用 .
运算符访问;当拥有一个结构体指正的时候,那么……还是可以用 .
运算符访问。当然。你想用 (*ptr).member
访问也可以呀,只是 Go 允许我们使用隐式间接引用:
func main() {
v := Vertex{1, 2}
p := &v
v.X = 1e8
fmt.Println(v)
p.X = 1e9
fmt.Println(v)
}
上文的赋值法,默认是第一个值赋给 X,第二个赋给 Y;对结构体的具体字段赋值则类似 JS 中的语法:
var (
v1 = Vertex{1, 2} // has type Vertex
v2 = Vertex{X: 1} // Y:0 is implicit
v3 = Vertex{} // X:0 and Y:0
p = &Vertex{1, 2} // has type *Vertex
)
特殊的前缀 & 返回一个指向结构体的指针。
0x13 数组
定义一个数组的格式:var arrayName [arrayLength]typeName
,注意是否空格。
如:var a [10]int
,a 是一个长度为 10 的 int 型数组。这样定义的数组是静态的,也就是说你定义完之后,a 的长度只能是 10 不能再改了。
0x14 切片
每个数组的大小都是固定的。而切片则为数组元素提供动态大小的、灵活的视角。在实践中,切片比数组更常用。类型 []T 表示一个元素类型为 T 的切片。
切片通过两个下标来界定,即一个上界和一个下界,二者以冒号分隔:a[low : high]
它会选择一个半开区间,包括第一个元素,但排除最后一个元素:[low, high)
。如切片 a[1:4]
,它包含 a 中下标从 1 到 3 的元素。
package main
import "fmt"
func main() {
primes := [6]int{2, 3, 5, 7, 11, 13}
var s []int = primes[1:4]
fmt.Println(s)
}
切片并不存储任何数据,只是描述了底层数组中的一段。更改切片的元素会修改其底层数组中对应的元素。与它共享底层数组的切片都会观测到这些修改(可以理解为切片是对一个数组的部分引用):
package main
import "fmt"
func main() {
names := [4]string{
"John",
"Paul",
"George",
"Ringo",
}
fmt.Println(names)
a := names[0:2]
b := names[1:3]
fmt.Println(a, b)
b[0] = "XXX"
fmt.Println(a, b)
fmt.Println(names)
}
切片的默认下界为 0,上界为切片/数组的长度。
切片拥有 长度 和 容量 两个属性。切片的长度就是它实际包含的元素个数;切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数(最多可以容纳的元素个数)。
切片 s
的长度和容量可通过表达式 len(s)
和 cap(s)
来获取。
package main
import "fmt"
func main() {
s := []int{2, 3, 5, 7, 11, 13}
printSlice(s)
// Slice the slice to give it zero length.
s = s[:0]
printSlice(s)
// Extend its length.
s = s[:4]
printSlice(s)
// Drop its first two values.
s = s[2:]
printSlice(s)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
输出:
len=6 cap=6 [2 3 5 7 11 13]
len=0 cap=6 []
len=4 cap=6 [2 3 5 7]
len=2 cap=4 [5 7]
切片的零值是 nil
,类似其他语言的 null
。nil
切片的长度和容量为 0 且没有底层数组。
var a [10]int // 声明的是数组
var b []int // 声明的是一个 nil 切片
0x15 make
内建函数 make()
可以创建切片,也是创造动态数组的方式。make 函数会分配一个元素为零值的数组并返回一个引用了它的切片,格式如下:
var name = make([]type, length) // len(name) = length
// e.g.
a := make([]int, 5) // len(a) = 5
若要指定该切片的容量,则需要向 make()
传入多余的参数:
name := make([]type, from, to) // len(name) = 0, cap(name) = to
如:
b := make([]int, 0, 5) // len(b)=0, cap(b)=5
b = b[:cap(b)] // len(b)=5, cap(b)=5 emmmm
b = b[1:] // len(b)=4, cap(b)=4
切片套切片就成了二位切(shu)片(zu);切片可以包含任意类型。
既然是动态数(qie)组(pian)就要可以动态修改数据嘛,比如向切片里添加新元素。Go 内建的 append(s []T, vs ... T) []T
函数就可以做到。
从函数原型中我们知道,append()
函数第一个参数是一个任意类型 T
的切片 s
(如果这里理解 C++ 的 template 就更容易理解了),接下来的几个参数分别是要加入切片 s
的 T
型数据,最后返回一个新的 T
型切片。新添加的值会出现在切片末尾。
当 s 的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。返回的切片会指向这个新分配的数组。参考。
0x16 range 遍历
对标 foreach,在 Go 中的遍历仍然用的是 for,但是有一个新的辅助关键字 range:当使用 for 循环遍历切片或映射的时候格式如下:
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
func main() {
for index, value := range pow {
fmt.Printf("2**%d = %d\n", index, value)
}
}
注意 range
循环时每次迭代会返回两个值 index
和 value
,第一个值是在切片中的下表,第二个值则是真正的值(但是是一个副本,而不是引用)。
如果我们只需要值不需要下标?你会说多写一个不会死,但是 Go 是不允许无用变量出现的;将 index
用 _
代替即可:
for _, value := range pow {
// ...
}
如果不需要值只要下标,去掉 , value
即可:
for index := range pow {
// ...
}
0x17 映射
数组只能以数字做下标,有时候我想以一个字符串啥的做下表怎么办?可以用 map
映射,这个就类似 C++ STL 的 map 的应用,只不过方法不同而已(
创建一个映射并赋值(注意空格)
var mapName map[keyType]valueType
// e.g. -----------
var m map[string]int
func main() {
m = make(map[string]int)
m["abc"] = 123
fmt.Println(m["abc"])
}
对 map 直接赋值:
package main
import "fmt"
type Vertex struct {
Lat, Long float64
}
var m = map[string]Vertex{
"Bell Labs": Vertex{
40.68433, -74.39967,
},
"Google": Vertex{
37.42202, -122.08408,
},
}
func main() {
fmt.Println(m)
}
上面对 m
的赋值中,可以对成员省略类型名:
var m = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},
"Google": {37.42202, -122.08408},
}
注意到,数组和映射的最后一个成员末尾要留一个逗号,这是 Go 的规范。
一些对映射的基本操作:
m[key] = element // 在映射 m 中插入元素
elem := m[key] // 获取元素
delete(mapInstance, key) // 从 mapInstance 中删除键值为 key 的元素
elem, ok := m[key] // 检测元素是否存在,若存在则 ok 为 true(还有这种操作??)
0x18 函数值和函数的闭包
函数本身是一种数据类型。函数可以是一个闭包,引用其函数体外的变量。
package main
import "fmt"
func adder() func(int) int {
sum := 0
// 返回的是一个函数闭包
return func(x int) int {
sum += x // 用了外部变量 sum
return sum
}
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i),
neg(-2*i),
)
}
}