1. 标识符
  • 字母或下划线开头
  • 之后只能出现数字、字母、下划线
  • 大小写敏感
Go语言关键字
breakdefaultfuncinterfaceselect
casedefergomapstruct
chanelsegotopackageswitch
constfallthroughifrangetype
continueforimportreturnvar
inttruetrue false iota nilint int8 int16 int32 int64uint uint8 uint16 uint32 uint64 uintptrfloat32 float64 complex128 complex64bool byte rune string errormake len cap new append copy close deletecomplex real imagpanic recover

这些内部预先定义的名字并不是关键字,你可以再定义中重新使用它们。在一些特殊的场景中重新定义它们也是有意义的,但是也要注意避免过度而引起语义混乱。

xxxYyyZzzXXX_YYY_ZZZXxxYyyZzzXxxYyyZzzIDHTTP
2. 基本类型

2.1 整型

类型符号长度范围
uint8无符号8位整型 (0 到 255)
uint16无符号16位整型 (0 到 65535)
uint32无符号32位整型 (0 到 4294967295)
uint64无符号64位整型 (0 到 18446744073709551615)
int8有符号8位整型 (-128 到 127)
int16有符号16位整型 (-32768 到 32767)
int32有符号32位整型 (-2147483648 到 2147483647)
int64有符号64位整型 (-9223372036854775808 到 9223372036854775807)
Unicoderuneint32byteuint8byte

有符号整数采用补码表示

intuint uintptr
intuintuintptrintint32intintint32

2.2 浮点型

float32float64mathmath.MaxFloat32float323.4e38math.MaxFloat641.8e3081.4e-454.9e-324
float32float64float64float32float32
==
.7071.eE
const Avogadro = 6.02214129e23  // 阿伏伽德罗常数
const Planck = 6.62606957E-34   // 普朗克常数

如果一个函数返回的浮点数结果可能失败,最好的做法是用单独的标志报告失败,像这样:

func compute() (value float64, ok bool) {
    // ...
    if failed {
        return 0, false
    }
    return result, true
}

2.3 复数

complex64complex128float32float64complexrealimag
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
i3.141592i2i0
fmt.Println(1i * 1i) // (-1+0i), i^2 = -1
1+2i2i+1xy
x := 1 + 2i
y := 3 + 4i
==!=

浮点数的相等比较是危险的,需要特别小心处理精度问题。想想Java的BigInteger!

2.4 布尔型

truefalseiffor==
if
i := 0
if b {
    i = 1
}

如果需要经常做类似的转换, 包装成一个函数会更方便:

// btoi returns 1 if b is true and 0 if false.
func btoi(b bool) int {
    if b {
        return 1
    }
    return 0
}

数字到布尔型的逆转换则非常简单, 不过为了保持对称, 我们也可以包装一个函数:

// itob reports whether i is non-zero.
func itob(i int) bool { return i != 0 }

2.5 字符串

字符串是 UTF-8 字符的一个序列(当字符为 ASCII 码时则占用 1 个字节,其它字符根据需要占用 2-4 个字节)。UTF-8 是被广泛使用的编码格式,是文本文件的标准编码,其它包括 XML 和 JSON 在内,也都使用该编码。由于该编码对占用字节长度的不定性,Go 中的字符串里面的字符也可能根据需要占用 1 至 4 个字节,这与其它语言如 C++、Java 或者 Python 不同(Java 始终使用 2 个字节)。Go 这样做的好处是不仅减少了内存和硬盘空间占用,同时也不用像其它语言那样需要对使用 UTF-8 字符集的文本进行编码和解码。

字符串是一种值类型,且值不可变,即创建某个文本后你无法再次修改这个文本的内容;更深入地讲,字符串是字节的定长数组。

Go 支持以下 2 种形式的字面值:

`This is a raw string \n` 中的 `\n\` 会被原样输出。
\0
lenrunes[i]ii0 ≤ i < len(s)
s := "hello, world"
fmt.Println(len(s)) // "12"
fmt.Println(s[0], s[7]) // "104 119" ('h' and 'w')
panic
s := "hello, world"
c := s[len(s)] // panic: index out of range
iiASCIIUTF8
+
s := "hello, world"
fmt.Println("goodbye" + s[5:]) // "goodbye, world"
+
==<

字符串的值是不可变的:一个字符串包含的字节序列永远不会被改变,当然我们也可以给一个字符串变量分配一个新字符串值。可以像下面这样将一个字符串追加到另一个字符串:

s := "left foot"
t := s
s += ", right foot"
s+=t
fmt.Println(s) // "left foot, right foot"
fmt.Println(t) // "left foot"

因为字符串是不可修改的,因此尝试修改字符串内部数据的操作也是被禁止的:

s[0] = 'L' // compile error: cannot assign to s[0]
ss[7:]
&str[i]

2.6 指针

*
 **TT*Tnil
type Point3D struct{ x, y, z float64 }
var pointer *Point3D
var i *[4]int

上面定义了两个指针类型变量。它们的值为nil,这时对它们的反向引用是不合法的,并且会使程序崩溃。

package main
func main() {
	var p *int = nil
	*p = 0
}
// in Windows: stops only with: <exit code="-1073741819" msg="process crashed"/>
// runtime error: invalid memory address or nil pointer dereference

虽然Go 语言和 C、C++ 这些语言一样,都有指针的概念,但是指针运算在语法上是不允许的。这样做的目的是保证内存安全。从这一点看,Go 语言的指针基本就是一种引用。

指针的一个高级应用是可以传递一个变量的引用(如函数的参数),这样不会传递变量的副本。当调用函数时,如果参数为基础类型,传进去的是值,也就是另外复制了一份参数到当前的函数调用栈。参数为引用类型时,传进去的基本都是引用。而指针传递的成本很低,只占用 4B或 8B内存。

如果代码在运行中需要占用大量的内存,或很多变量,或者两者都有,这时使用指针会减少内存占用和提高运行效率。被指向的变量保存在内存中,直到没有任何指针指向它们。所以从它们被创建开始就具有相互独立的生命周期。

stringboolintfloat 

指针的使用方法:

*
package main

import "fmt"

func main() {
	var a, b int = 20, 30 // 声明实际变量
	var ptra *int         // 声明指针变量
	var ptrb *int = &b

	ptra = &a // 指针变量的存储地址

	fmt.Printf("a  变量的地址是: %x\n", &a)  // a  变量的地址是: c00000a0c8
	fmt.Printf("b  变量的地址是: %x\n", &b)  // b  变量的地址是: c00000a0e0

	// 指针变量的存储地址
	fmt.Printf("ptra  变量的存储地址: %x\n", ptra)  // ptra  变量的存储地址: c00000a0c8
	fmt.Printf("ptrb  变量的存储地址: %x\n", ptrb)  // ptrb  变量的存储地址: c00000a0e0

	// 使用指针访问值
	fmt.Printf("*ptra  变量的值: %d\n", *ptra)  // *ptra  变量的值: 20
	fmt.Printf("*ptrb  变量的值: %d\n", *ptrb)  // *ptrb  变量的值: 30
}

2.7 type关键字

一个类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的

type 新类型 底层类型  // 自定义类型,本质2个类型
type 别名 = 已有类型  // 类型别名,本质一个类型

类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在外部包也可以使用。

为了说明类型声明,我们将不同温度单位分别定义为不同的类型:

// Package tempconv performs Celsius and Fahrenheit temperature computations.
package tempconv

import "fmt"

type Celsius float64     // 摄氏温度
type Fahrenheit float64  // 华氏温度

const (
    AbsoluteZeroC Celsius = -273.15 // 绝对零度
    FreezingC Celsius = 0           // 结冰点温度
    BoilingC Celsius = 100          // 沸水温度
)

func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
Celsius(t)Fahrenheit(t)Celsius(t)Fahrenheit(t)CToFFToC
T(x)xTTT(*int)(0)xTxT

在任何情况下,运行时不会发生转换失败的错误( 错误只会发生在编译阶段)

CelsiusFahrenheitfloat64
fmt.Printf("%g\n", BoilingC-FreezingC)       // "100" °C
boilingF := CToF(BoilingC)
fmt.Printf("%g\n", boilingF-CToF(FreezingC)) // "180" °F
fmt.Printf("%g\n", boilingF-FreezingC)       // compile error: type mismatch
3. 变量声明

3.1 基本用法

var
var 变量名字 类型 = 表达式

其中 “类型” 或 “= 表达式” 两个部分可以省略其中的一个

0false空字符串slicemapchan函数nil

也可以在一个声明语句中同时声明一组变量,或用一组初始化表达式声明并初始化一组变量。如果省略每个变量的类型,将可以声明多个类型不同的变量(类型由初始化表达式推导):

var i, j, k int  // int, int, int
var b, f, s = true, 2.3, "four"  // bool, float64, string

初始化表达式可以是字面量或任意的表达式。在包级别声明的变量会在main入口函数执行前完成初始化,局部变量将在声明语句被执行到的时候完成初始化。

一组变量也可以通过调用一个函数,由函数返回的多个返回值初始化:

var f, err = os.Open(name) // os.Open returns a file and an error

类似Python返回的元组

还可以这样声明变量:

var (
	a int
	b bool
	str string
)

这种因式分解关键字的写法一般用于声明全局变量。

3.2 简短变量声明

在函数内部,有一种称为简短变量声明语句的形式可用于声明和初始化局部变量。它的形式为:

名字:= 表达式
anim := gif.GIF{LoopCount: nframes}
freq := rand.Float64() * 3.0
t := 0.0

因为简洁和灵活的特点,简短变量声明被广泛用于大部分的局部变量的声明和初始化。var形式的声明语句往往是用于需要显式指定变量类型地方,或者因为变量稍后会被重新赋值而初值无关紧要的地方

i := 100 // an int
var boiling float64 = 100 // a float64
var names []string
var err error
var p Point
var
i, j := 0, 1
for
:==
varos.Open
f, err := os.Open(name)
if err != nil {
    return err
}
// ...use f...
f.Close()
in, err := os.Open(infile)
// ...
out, err := os.Create(outfile)
f, err := os.Open(infile)
// ...
f, err := os.Create(outfile) // compile error: no new variables

3.3 声明指针

xx[i]x.f

一个指针的值是另一个变量的地址。一个指针对应变量在内存中的存储位置。并不是每一个值都会有一个内存地址,但是对于每一个变量必然有对应的内存地址。通过指针,我们可以直接读或更新对应变量的值,而不需要知道该变量的名字(如果变量有名字的话)。

var x intx&xx *intintppxpx*p p*pint*p
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"

对于聚合类型每个成员,比如结构体的每个字段、或者是数组的每个元素,也都是对应一个变量,因此可以被取地址。

nilp != nil pnil
var x, y int
fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false"

var a, b *int
fmt.Println(a,b)  //<nil> <nil>
var a, b *intab
fvp
var p = f()

func f() *int {
    v := 1
    return &v
}
f
fmt.Println(f() == f()) // "false"

因为指针包含了一个变量的地址,因此如果将指针作为参数调用函数,那将可以在函数中通过该指针来更新变量的值。例如下面这个例子就是通过指针来更新变量的值,然后返回更新后的值,可用在一个表达式中:

func incr(p *int) int {
    *p++ // 非常重要:只是增加p指向的变量的值,并不改变p指针!!!
    return *p
}

v := 1
incr(&v) // side effect: v is now 2
fmt.Println(incr(&v)) // "3" (and v is 3)
*pvslicemapchan结构体数组接口

所谓别名,是因为这些都是指向相同内存地址的标识符。

3.4 new函数声明

newnew(T)TT*T
p := new(int) // p, *int 类型, 指向匿名的 int 变量
fmt.Println(*p) // "0"
*p = 2 // 设置 int 匿名变量的值为 2
fmt.Println(*p) // "2"
newnew(T)new
new
p := new(int)
q := new(int)
fmt.Println(p == q) // "false"
struct{}[0]int 
new
newnew

3.5 变量的生命周期

变量的生命周期指的是在程序运行期间变量有效存在的时间间隔

  • 对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的

  • 而相比之下,局部变量的生命周期则是动态的:从每次创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。

for t := 0.0; t < cycles*2*math.Pi; t += res {
x := math.Sin(t)
y := math.Sin(t*freq + phase)
img.SetColorIndex(
  size+int(x*size+0.5), size+int(y*size+0.5),
  blackIndex, // 最后插入的逗号不会导致编译错误,这是Go编译器的一个特性
) // 小括弧另起一行缩进,和大括弧的风格保存一致
}

Go语言的自动圾收集器是如何知道一个变量是何时可以被回收的呢?基本的实现思路是,从每个包级的变量和每个当前运行函数的每一个局部变量开始,通过指针或引用的访问路径遍历,是否可以找到该变量。如果不存在这样的访问路径,那么说明该变量是不可达的,也就是说它是否存在并不会影响程序后续的计算结果。

因为一个变量的有效周期只取决于是否可达,因此一个循环迭代内部的局部变量的生命周期可能超出其局部作用域。同时,局部变量可能在函数返回之后依然存在

varnew
var global *int

func f() {
    var x int
    x = 1
    global = &x
}

func g() {
    y := new(int)
    *y = 1
}
fxglobalxfg*y *yg*ynew

其实在任何时候,你并不需为了编写正确的代码而要考虑变量的逃逸行为,要记住的是,逃逸的变量需要额外分配内存,同时对性能的优化可能会产生细微的影响。

Go语言的自动垃圾收集器对编写正确的代码是一个巨大的帮助,但也并不是说你完全不用考虑内存了。你虽然不需要显式地分配和释放内存,但是要编写高效的程序你依然需要了解变量的生命周期。例如,如果将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收(从而可能影响程序的性能)。

4. 常量
const

存储在常量中的数据类型只可以是布尔型数字型(整数型、浮点型和复数)和字符串型

const identifier [type] = value
const Pi = 3.14159
[type]
const b string = "abc"const b = "abc"

常量的值必须是能够在编译时就能够确定的;你可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。

const c1 = 2/3const c2 = getNumber()getNumber() used as value
len()

数字型的常量是没有大小和符号的,并且可以使用任何精度而不会导致溢出:

const Ln2 = 0.693147180559945309417232121458\
                       176568075500134360255254120680009
const Log2E = 1/Ln2 // this is a precise reciprocal
const Billion = 1e9 // float constant
const hardEight = (1 << 100) >> 97
\

一个常量的声明也可以包含一个类型和一个值,但是如果没有显式指明类型,那么将从右边的表达式推断类型

如果是批量声明的常量,除了第一个外其它的常量右边的初始化表达式都可以省略,如果省略初始化表达式则表示使用前面常量的初始化表达式写法,对应的常量类型也一样的。例如:

const (
    a = 1
    b
    c = 2
    d
)
fmt.Println(a, b, c, d) // "1 1 2 2"
iota

4.1 iota常量生成器

iotaconstiota01constiota
const (
    a = iota // 0
    b = 4  // iota=1
    c = iota // 2
    d = iota + 2 //iota=3, 3 + 5 = 5
    e = 34  // iota=4
    f = 25  // iota=5
    g = iota // 6
)
const (
    h = 5  // iota=0
    i = iota  // 1
    j = 6  // iota=2
    k = iota  // 3
)
constiotaiotaconst iota 0
5. 赋值

和其他语言一样,值放在等号右边,接收的变量放在等号左边

x = 1 // 命名变量的赋值
*p = true // 通过指针间接赋值
person.name = "bob" // 结构体字段赋值
count[x] = count[x] * scale // 数组、slice或map的元素赋值

也有二元运算符,如:

count[x] *= scale
++-- x = i++
v := 1
v++ // 等价方式 v = v + 1;v 变成 2
v-- // 等价方式 v = v - 1;v 变成 1

5.1 元组赋值

元组赋值是另一种形式的赋值语句,它允许同时更新多个变量的值。在赋值之前,赋值语句右边的所有表达式将会先进行求值,然后再统一更新左边对应变量的值。这对于处理有些同时出现在元组赋值语句左右两边的变量很有帮助,例如我们可以这样交换两个变量的值:

x, y = y, x
a[i], a[j] = a[j], a[i]

或者是计算两个整数值的的最大公约数:

func gcd(x, y int) int {
    for y != 0 {
        x, y = y, x%y
    }
    return x
}

或者是计算斐波纳契数列(Fibonacci)的第N个数:

func fib(n int) int {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        x, y = y, x + y
    }
    return x
}

元组赋值也可以使一系列琐碎赋值更加紧凑,特别是在for循环的初始化部分:

i, j, k = 2, 3, 5

但如果表达式太复杂的话,应该尽量避免过度使用元组赋值;因为每个变量单独赋值语句的写法可读性会更好。有些表达式会产生多个值,比如调用一个有多个返回值的函数。当这样一个函数调用出现在元组赋值右边的表达式中时(右边不能再有其它表达式),左边变量的数目必须和右边一致。

f, err = os.Open("foo.txt") // function call returns two values
_
_, err = io.Copy(dst, src) // 丢弃字节数
_, ok = x.(T)              // 只检测类型,忽略具体值

5.2 可赋值性

赋值语句是显式的赋值形式,但是程序中还有很多地方会发生隐式的赋值行为:函数调用会隐式地将调用参数的值赋值给函数的参数变量,一个返回语句将隐式地将返回操作的值赋值给结果变量,一个复合类型的字面量也会产生赋值行为。

不管是隐式还是显式地赋值,在赋值语句左边的变量和右边最终的求到的值必须有相同的数据类型。更直白地说,只有右边的值对于左边的变量是可赋值的,赋值语句才是允许的

nil
==!=
6. 值类型和引用类型
intfloatbool string 数组struct
=j = ii
package main

import (
	"fmt"
)

func main() {
	i := 7
	j := i
	fmt.Println(i, &i) // 7 0xc00000a0c8
	fmt.Println(j, &j) // 7 0xc00000a0e0
}
指针slicesmapschannel
r1 r1 

同一个引用类型的指针指向的多个字可以是在连续的内存地址中(内存布局是连续的),这也是计算效率最高的一种存储形式;也可以将这些字分散存放在内存中,每个字都指示了下一个字所在的内存地址。

r2 = r1
r1 r2 
7. 运算符

7.1 算数运算符

运算符描述实例
+相加A + B 输出结果 30
-相减A - B 输出结果 -10
*相乘A * B 输出结果 200
/相除B / A 输出结果 2
%求余B % A 输出结果 0
++自增A++ 输出结果 11
--自减A-- 输出结果 9
/%++--

7.2 关系运算符

运算符描述实例
==检查两个值是否相等,如果相等返回 True 否则返回 False。(A == B) 为 False
!=检查两个值是否不相等,如果不相等返回 True 否则返回 False。(A != B) 为 True
>检查左边值是否大于右边值,如果是返回 True 否则返回 False。(A > B) 为 False
<检查左边值是否小于右边值,如果是返回 True 否则返回 False。(A < B) 为 True
>=检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。(A >= B) 为 False
<=检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。(A <= B) 为 True

7.3 逻辑运算符

运算符描述实例
&&逻辑 AND 运算符。 如果两边的操作数都是 True,则条件 True,否则为 False。(A && B) 为 False
||逻辑 OR 运算符。 如果两边的操作数有一个 True,则条件 True,否则为 False。(A || B) 为 True
!逻辑 NOT 运算符。 如果条件为 True,则逻辑 NOT 条件 False,否则为 True。!(A && B) 为 True

注意,Go语言中逻辑运算是短路运算。

7.4 位运算符

pqp & qp | qp ^ q
00000
01011
11110
10011
^^p&^1 &^ 1 = 01 &^ 0 = 10 &^ 1 = 00 &^ 0 = 0a&(^b)5 &^ 3 = 4101 &^ 011 = 100

7.5 复合赋值运算符

+=-=

7.6 指针和地址运算符

运算符描述实例
&返回变量存储地址&a; 将给出变量的实际地址
*取出指针指向的值*a; 返回指针指向的值
func main() {
	a := 1
	b := &a
	fmt.Println(*b)  // 1
	fmt.Println(&b)  // 0xc0000ca018
}
8. 作用域

一个声明语句将程序中的实体和一个名字关联,比如一个函数或一个变量。声明语句的作用域是指源代码中可以有效使用这个名字的范围。

不要将作用域和生命周期混为一谈。声明语句的作用域对应的是一个源代码的文本区域;它是一个编译时的属性。一个变量的生命周期是指程序运行时变量存在的有效时间段,在此时间区域内它可以被程序的其他部分引用;是一个运行时的概念

语法块是由花括弧所包含的一系列语句,就像函数体或循环体花括弧对应的语法块那样。语法块内部声明的名字是无法被外部语法块访问的。语法决定了内部声明的名字的作用域范围。

intlentruetempconvfmtfmttempconv.CToFc
breakcontinuegoto
new

内部声明屏蔽了外部同名的声明,让外部的声明的名字无法被访问

forforii++ 
xforfor
func main() {
    x := "hello"
    for _, x := range x {
        x := x + 'A' - 'a'
        fmt.Printf("%c", x) // "HELLO" (one letter per iteration)
    }
}

特别的,下面这种代码的bug检测器可能失效

var cwd string

func init() {
    cwd, err := os.Getwd() // NOTE: wrong!
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
    log.Printf("Working directory = %s", cwd)
}
init()cwdcwdcwdcwd
9. 注释
//.../* ... */

9.1 包注释

  • 每个包都应该有一个包注释,一个位于package子句之前行注释
  • 包注释应该包含下面基本信息
// @Title  文件名称
// @Description  文件描述
// @Author  作者名称 (时间 格式是2019/3/26  19:53)
// @Update  修改者名称 (时间 格式是2019/3/26  19:53)

9.2 结构(接口)注释

结构体名, 结构体说明
// User , 用户对象,定义了用户的基础信息
type User struct{
    Username string // 用户名
    Email string // 邮箱
}

9.3 函数(方法)注释

  • 每个函数,或者方法(结构体或者接口下的函数称为方法)都应该有注释说明
  • 函数的注释应该包括三个方面
  • 注释在函数(方法)定义语句上面
// @title    函数名称
// @description   函数的详细描述
// @auth      作者             时间(2019/6/18   10:57 )
// @param     输入参数名        参数类型         "解释"
// @return    返回参数名        参数类型         "解释"

9.4 代码逻辑注释

对于一些关键位置的代码逻辑,或者局部较为复杂的逻辑,需要有相应的逻辑说明,方便其他开发者阅读该段代码,实例如下:

// 从 Redis 中批量读取属性,对于没有读取到的 id , 记录到一个数组里面,准备从 DB 中读取
xxxxx
xxxxxxx
xxxxxxx

9.5 注释风格

统一使用中文注释,对于中英文字符之间严格使用空格分隔, 这个不仅仅是中文和英文之间,英文和中文标点之间也都要使用空格分隔,例如:

// 从 Redis 中批量读取属性,对于没有读取到的 id , 记录到一个数组里面,准备从 DB 中读取