文章目录
  1. 1. hello world
  2. 2. 数据结构
    1. 2.1. 基本类型
      1. 2.1.1. bool
      2. 2.1.2. 整型
      3. 2.1.3. 浮点数
      4. 2.1.4. 复数
      5. 2.1.5. 特殊类型
      6. 2.1.6. 字符串
    2. 2.2. 数组
    3. 2.3. slice
    4. 2.4. map
    5. 2.5. 结构体
    6. 2.6. “枚举类型”
  3. 3. 语法结构
    1. 3.1.
      1. 3.1.1. 包内全局变量,初始化函数
    2. 3.2. 声明
    3. 3.3. 控制结构
      1. 3.3.1. if
      2. 3.3.2. switch
        1. 3.3.2.1. type switch
      3. 3.3.3. 循环
    4. 3.4. 函数
      1. 3.4.1. 闭包
    5. 3.5. 方法
    6. 3.6. 分配内存
      1. 3.6.1. new
      2. 3.6.2. make
  4. 4. Reference

平时工作语言是c/c++,做后端开发很强大,但是毕竟写起来还是麻烦了一些,缺乏现代语言特性。所以一直在找一门辅助语言,它有便捷的语法,可以随手写一些小工具甚至小服务啥的。之前考虑过node.js,写c bundle感觉还可以,但是v8的用法感觉写起来有点麻烦,而且最重要的是node里操作二进制数据太麻烦,日常工作中一半的工作都是和二进制打交道,所以node并不太合适。并且js的语法也相对比较随意,写起来不那么规范。正好最近感觉自己好久没有学习新语言了,于是打算研究研究golang,作为面向concurrency的语言之一,Scala/Erlang是跑在虚拟机上的语言,在现在的生产环境中用起来有点维护麻烦,同时这种fp的语言对我来说学习成本也比较高,所以看起来还是go更合适一些。

hello world

1
2
3
4
5
package main
import "fmt"
func main() {
fmt.Println("hello world")
}

运行$ go run hello.go,编译$ go build -o hello hello.go

数据结构

基本类型

bool

布尔类型,truefalse,没啥好说的

整型

1
2
3
4
5
6
7
8
9
uint8 //the set of all unsigned 8-bit integers (0 to 255)
uint16 //the set of all unsigned 16-bit integers (0 to 65535)
uint32 //the set of all unsigned 32-bit integers (0 to 4294967295)
uint64 //the set of all unsigned 64-bit integers (0 to 18446744073709551615)
int8 //the set of all signed 8-bit integers (-128 to 127)
int16 //the set of all signed 16-bit integers (-32768 to 32767)
int32 //the set of all signed 32-bit integers (-2147483648 to 2147483647)
int64 //the set of all signed 64-bit integers (-9223372036854775808 to 9223372036854775807)

浮点数

1
2
float32 //the set of all IEEE-754 32-bit floating-point numbers
float64 //the set of all IEEE-754 64-bit floating-point numbers

复数

1
2
complex64 //the set of all complex numbers with float32 real and imaginary parts
complex128 //the set of all complex numbers with float64 real and imaginary parts

特殊类型

1
2
3
4
5
6
byte //alias for uint8
rune //alias for int32
uint //either 32 or 64 bits
int //same size as uint
uintptr //an unsigned integer large enough to store the uninterpreted bits of a pointer value

记不清之前在哪个文档上看的,说int无论在32位还是64位机器上,都是32位的,但看Language Specification上还是说int的长度和机器的架构有关系,可能之前版本的go设计是统一32位,但由于某些原因,最新版本还是将int的设计为架构相关,这点和c不太一样。不过明确声明长度,用uint64之类的还是一个很好的习惯,循环之类的用用int还行。

字符串

go的字符串和c有点像,但go的字符串是不可改变的,实际上c的静态字符串也是这样的,可以通过下标来取得每个字节的内容,和c是一模一样的,len函数用来求长度,类似strlen。不可以对s[i]取址,即&s[i]是错误的。

数组

go的数组有点像python,提供各种切片操作。这点要比c灵活得多。go的数组是值,所以赋值时会复制每个元素。go的数组长度是它的类型的一部分,也就是说[5]int[10]int是两种类型。如果go的数组要实现传址,必须要显示的求址才行,不过go的语法不推荐这么使用数组,go的语法习惯是使用切片(slice),感觉切片叫着别扭,以后就统一叫slice好了。

1
2
3
4
//类似int arr[5];
var arr [5]int
//初始化了int arr[] = {1, 2, 3, 4, 5};
b := [5]{1, 2, 3, 4, 5}

slice

slice可以看做是一个特殊的数组,其实和c里面的数组非常像。slice作为参数时是传址的,也就是可以在函数内部修改参数中的slice并且外部看得到变化。slice可以做一些类似python中的下标范围操作。slice有两个重要的内建函数,len是返回当前数据的长度,而cap是返回最大容量。特殊的len(nil)=0, cap(nil)=0

1
2
3
4
5
6
7
8
//新建一个len=5, cap=5的slice
sli := make([]byte, 5)
//新建一个len=0, cap=10的slice
sli2 := make([]int, 0, 10)
sli2 = []int{1, 2, 3, 4, 5} //len(sli2)=5, cap(sli2)=10
fmt.Println(sli2[0:3])//{1, 2, 3}
fmt.Println(sli2[:3])//{1, 2, 3}
fmt.Println(sli2[2:])//{3, 4, 5}

map

伟大领袖xxx曾经说过,有哈希的语言都是好语言,嗯,没错,这是一个很常用的数据结构,go这种现代语言当然是内置的。hash和数组/slice很像,就像一个key(下标)不一定是整数的数组。如果尝试取一个hash中没有的值,会返回值类型的零值。任何定义了相等操作的类型都可以作为key的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
mp := map[string]int {
"hello": 1,
"world": 2,//这里的,是允许的
}
//使用map的值很简单
fmt.Println(mp["hello"])
if v, exists := mp["no"]; exists {
fmt.Printf("exists %d\n", v);
} else {
fmt.Printf("not exists %d\n", v);//not exists 0
}
//删除一个k-v
delete(mp, "hello")

结构体

go没有java/c++里类的概念,但是它有结构体,和c里的结构体类似。结构体中首字母大写的字段可以在外部被访问,小写开头的变量不能被外部访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//定义一个结构体
type Intint struct {
A uint64
B uint64
}
//初始化赋值
ii := Intint{1, 2}
//指定某个成员
ii2 := Intint{A:1}//B=0
//全自动初始化为零值
ii3 := Intint{}
fmt.Println(unsafe.Sizeof(ii))//16
//声明并初始化赋值一个结构体指针
ip := &Intint{2, 3}
//使用new创建指针
ip2 := new(Intint)//type = *Intint

“枚举类型”

go不存在真正意义上的枚举类型,但可以通过自定义类型常量来实现,例如以下例子:

1
2
3
4
5
6
7
8
9
//自定义一个LogLevel类型作为enum的类型
type LogLevel int
const (
//iota表示枚举初始值,这里是整型,所以是0, 这里声明成LogLevel型,可以实现类型约束
LOG_L_DEBUG LogLevel = iota
LOG_L_NOTICE
LOG_L_WARNING
LOG_L_FATAL
)

语法结构

go使用package的概念来管理模块,可以有目录层次结构,导入使用import关键字,如果涉及到目录层次结构,一般程序里可以直接用最后一个元素来指代。在源文件的开头以package关键字声明这个程序属于哪个包。

1
2
3
4
5
6
7
8
//属于test包
package test
//引入俩包,第二个用rand.Xxxx来使用
import (
"fmt"
"math/rand"
)

包内全局变量,初始化函数

当需要定义一些复杂的全局变量时,可以直接在函数体外定义,赋值可以是一些运行时确定的表达式,包内可以有多个名为init的函数,这个函数在初始化全局变量后会执行。每个包的init函数只执行一次,并且全局变量也指初始化赋值1次。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//./util/a.go
package util
import "./logger"
var (
A = 1
)
func What() int {
return logger.What()
}
//./util/logger/b.go
package logger
import "fmt"
var (
a = 0
)
func init() {
fmt.Println("init has been invoked.")
a++
}
func What() int {
return a
}
func SetWhat(v int) {
a = v
}
//./pkg.go
package main
import (
"./util"
"./util/logger"
"fmt"
)
func main() {
fmt.Println(logger.What())
logger.SetWhat(1000)
fmt.Println(logger.What())
fmt.Println(util.What())
}
//go run pkg.go
init has been invoked.
1
1000
1000

声明

golang是一门静态类型语言,支持类型推导,声明变量赋值可以采用平行赋值,比较方便。常量因为是编译时必须确定值,所以只能是基本数值类型或者字符串。比如:

1
2
3
4
5
6
7
8
var test int = 1
var a, b, c int = 1, 2, 3
// 类型推到之一
var needType = true
// 类型推到之二,等价于var msg string = "hello world!"
msg := "hello world!"
//常量声明
const pi float = 3.1415926

go对声明未赋值的变量统一初始化为其零值,如整数为0,字符串为空串””等等。

控制结构

if

go的if和c很像,就是没有那个小括号,并且强制要求{必须在上一行的末尾,而不是独占一行。嗯,对于这个用法我很习惯啊,也终于结束了c/c++中{到底在哪的争论。不过不写()还是有些不习惯的。因为go的switch非常强大,所以如果if的分支比较多时,推荐使用switch代替。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if a > b {
doSomething()
}
//也可以先赋值在判断条件
if f, err := os.Open(name); err != nil {
fmt.Println(err)
}
//多分支
if a < 0 {
doSth1()
} else if a == 0 {
doSth2()
} else {
doSth3()
}

switch

go的switch要比c强大一些,除了枚举数值之外,还可以加入一些条件判断,取代if-else-if-else的结构。与c不同的是没有fallthrough,多值的情况用,分隔即可,也可以通过fallthrough关键字强制向下执行。

1
2
3
4
5
6
7
8
9
10
switch {
case a<10 && a>0:
do1()
case a>=10 && a<100:
do2()
case a>=100 && a<1000:
do3()
default:
do4()
}

type switch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
fmt.Printf("unexpected type %T", t) // %T prints whatever type t has
case bool:
fmt.Printf("boolean %t\n", t) // t has type bool
case int:
fmt.Printf("integer %d\n", t) // t has type int
case *bool:
fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}

循环

go只有for一个循环关键字,while的功能也被for取代,用法也比c强大一些,还比较简洁。for-range操作时,一个基本单位是rune,如果是一个中文字符串,那么每次迭代都是其unicode编码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//最常见的
for i:=0; i<100; i++ {
do(i)
}
//我是while
i := 0
for i<100 {
do(i)
i++
}
//我是死循环
for {
do()
}
//foreach
arr := [3]int{1, 2, 3}
for i, v := range arr {
fmt.Printf("%d=>%d\n", i, v)
}
//1=>1\n2=>2\n3=>3\n

函数

和各种函数式编程语言一样,go也是first-class function,函数可以当做一个值来传递,也就有了各种匿名函数的用法。同时,自然也是支持闭包的。与c不同的是,go的函数可以返回多个值,这个用法在go里应用非常广泛,标准库中很多函数都是以错误作为第二个返回值返回的。另外go也支持命名返回值,也就是给出返回值的变量名,然后函数里可以直接赋值,最后直接return即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//一个2个参数2个返回值的函数
func some(a, b int) (int, int) {
return a+b, a*b
}
//不定参数
func sum(nums ...int) int {
total := 0
for _, num := range nums {
total += num
}
return total
}
//匿名函数
f := func(a, b int) int {
return a+b
}
fmt.Println(f(5, 6))//11

闭包

但凡FP类的语言,都支持函数闭包。因为fp用的少,所以也不是特别清楚闭包的使用场景,大概意思就是函数实例化之后可以保存其局部变量,有点“小”对象的意思。来个斐波那契数列的例子吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//fibonacci是一个返回值类型为func() int的函数
func fibonacci() func() int {
//通过闭包实现保存局部变量a,b的值
a := 0
b := 1
return func() int {
sum := a + b
a = b
b = sum
return a
}
}
func main() {
//函数的“实例化”
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}

方法

go是没有“类”的,但是你可以给任何本包内的结构体添加方法。和函数不同,方法有一个method receiver,也就是这个方法是属于哪个结构的。事实上除了结构体,go可以给本包内除了基本类型以外的任何类型添加方法。但需要注意,最好以指针类型作为method receiver,否则按值传递可能会起不到效果,并且造成效率降低。

1
2
3
4
5
6
7
8
//一个方法的例子
type TwoInt struct {
A int
B int
}
func (this *TwoInt)Sum() int {
return this.A + this.B
}

分配内存

go有两种分配内存的方式,newmake

new

new(T)返回一个*T类型的指针,同时new还将所有的值赋为零值。可以理解为分配了一块全为0的内存空间。这个特性比较有用,省去了c/c++自己赋零值的麻烦。当有需要初始化为非零值,也就是相当于构造函数的功能,go习惯上命名为NewT的函数。例如:

1
2
3
4
//一个“构造函数”的例子
func NewTwoInt(a, b int) *TwoInt {
return &TwoInt{a, b}
}

make

go中make用于分配slice, map, channel,返回一个初始化(不一定是零值)的T类型的值,而new返回的是指针。

Reference

  1. Go 语言简介(上)— 语法
  2. Go 语言简介(下)— 特性
  3. A Tour of Go(FuckGFW)
  4. Go by Example
  5. Package Documentation(FuckGFW)
  6. Effective Go(FuckGFW)
  7. 学习Go语言
  8. The Go Programming Language Specification(FuckGFW)
文章目录
  1. 1. hello world
  2. 2. 数据结构
    1. 2.1. 基本类型
      1. 2.1.1. bool
      2. 2.1.2. 整型
      3. 2.1.3. 浮点数
      4. 2.1.4. 复数
      5. 2.1.5. 特殊类型
      6. 2.1.6. 字符串
    2. 2.2. 数组
    3. 2.3. slice
    4. 2.4. map
    5. 2.5. 结构体
    6. 2.6. “枚举类型”
  3. 3. 语法结构
    1. 3.1.
      1. 3.1.1. 包内全局变量,初始化函数
    2. 3.2. 声明
    3. 3.3. 控制结构
      1. 3.3.1. if
      2. 3.3.2. switch
        1. 3.3.2.1. type switch
      3. 3.3.3. 循环
    4. 3.4. 函数
      1. 3.4.1. 闭包
    5. 3.5. 方法
    6. 3.6. 分配内存
      1. 3.6.1. new
      2. 3.6.2. make
  4. 4. Reference