Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.

发展历史

诞生于2006年1月2日下午15点4分5秒

2009正式发布并开源

2012年 Go 1.0

TIOBE 排名第10(2020.3)

为什么需要 Go

环境搭建

官网下载安装包:https://golang.org/

国内镜像:https://golang.google.cn

添加环境变量:

cd ~
sudo vi .zshrc

添加下面的内容:

export PATH=$PATH:/usr/local/go/bin
export GOPATH=/Project/go
export GOBIN=$GOPATH/bin

重新载入配置:

source ~/.zshrc 

配置代理,否则国内无法正常安装包

go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.io,direct

这个代理由google提供,国内也有一些厂商提供了镜像。

目录结构划分

个人

GOPATH

企业

company

Hello,World

src目录下新建hello文件夹,新建hello.go文件

package main

import "fmt"

func main() {
	fmt.Println("Hello, World!")
}

还需要在项目文件夹下新建一个go.mod文件用于管理包,否则无法编译,这是go在1.1.1后加入的新特性。

编译文件:

go build

编译后会在项目文件夹生成hello文件

运行:

./hello

基本语法

变量

变量定义

var a,b,c bool
var s1,s2 string = "hello","world"

变量定义可以放在包内,也可以放在函数内

使用var()可以集中定义变量

var(
  aa = 3
  ss = "kkk"
  bb = true
)

变量类型并非必要手动声明,编译器可以根据变量的值来推断其类型:

var a,b,c = 'test',true,123

使用:=定义变量,可以不使用var,但只能在函数内使用

a,b,s1 := true,"hello",456

内建变量类型

bool
string
(u)int,(u)int8,(u)int16,(u)int32,(u)int64 // 不同C长度整数,u为无符号整数
uintptr // 指针
byte
rune // 字符型 类似char,针对多语言优化
float32,float64 // 浮点型
complex64,complex128 // 复数类型

强制类型转换

所有转换都必须显式说明:valueOfTypeB = typeB(valueOfTypeA)

a := 5.0
b := int(a)

类型 B 的值 = 类型 B (类型 A 的值)

var a,b int =3,4
var c int = math.Sqrt(a*a+b*b) // 出错
var c int = int(math.Sqrt(float64(a*a+b*b)))

常量

Go 语言的变量一旦定义,必须使用,否则编译报错。

常量定义

常量使用关键字 const 定义,用于存储不会改变的数据。

语法与变量相似

const filename = "abd.txt"  // 隐式类型定义,省略类型说明符
const a,b int =3,4  // 显式类型定义

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

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

枚举类型

const(
  cpp = 0
  java = 1
  python = 2
  golang = 3
)
const(
  // 自增值,和上面的定义等价,0,1,2,3
  cpp = iota
  java
  python
  golang
)
const(
  // 自增值
  cpp = iota  // 自增方法可以使用表达式,例如:1 << (10 * iota)
  _          // 跳过某个位置
  python
  golang
)

条件语句

if

if 布尔表达式 {
  /* 在布尔表达式为 true 时执行 */
}

if还有另外一种形式,包含一个statement可选语句部分,这个部分在条件判断之前运行。

if statement; condition {
  
}

例子:

if v > 100 {
  return 100
} else if v < 0 {
  return 0
} else {
  return v
}

if条件里可以赋值,if的条件里赋值的变量作用域就在这个if语句里

switch

var result int
switch op {
  case "+"
  	result = a+b
  case "-":
  	result = a-b
  case "*":
  	result = a*b
  case "/":
  	result = a/b
  default:
  	panic("unsupposed operator" + op)  // 报错
  return result
}

switch会自动break,除非使用fallthrough

可选的 default 分支可以出现在任何顺序,但最好将它放在最后。它的作用类似与 if-else 语句中的 else,表示不符合任何已给出条件时,执行相关语句。

switch后不一定需要表达式:

g := ""
switch {
  case score < 0 || score > 100:
    default:
  		panic(fmt.Sprintf("Wrong score: %d",score))
  case score < 60: 
  	g = "F"
  case score < 80:
  	g = "C"
 	case score < 90
  	g = "B"
  case score <= 100
  	g = "A"
  default:
  	panic(fmt.Sprintf("Wrong score: %d",score))
}

循环语句

for

Go 语言中只有for一种循环。

for init; condition; post {
  
}
/*
init: 一般为赋值表达式,给控制变量赋初值;
condition: 关系表达式或逻辑表达式,循环控制条件;
post: 一般为赋值表达式,给控制变量增量或减量。
*/

for条件里不需要括号,可以省略初始条件,结束条件,递增表达式,通过省略条件,可以当while使用。

sum := 0
for i:=1;i<=100;i++ {
  sum += i
}

标签与 goto

for、switch 或 select 语句都可以配合标签(label)形式的标识符使用,即某一行第一个以冒号(:)结尾的单词(gofmt 会将后续代码自动移至下一行)。

标签名称大小写敏感,为了提升可读性,一半建议使用全部大写字母

package main

import "fmt"

func main() {

LABEL1:
    for i := 0; i <= 5; i++ {
        for j := 0; j <= 5; j++ {
            if j == 4 {
                continue LABEL1
            }
            fmt.Printf("i is: %d, and j is: %d\n", i, j)
        }
    }

}
// continue 指向 LABEL1,当执行到该语句时,就会跳转到 LABEL1 标签位置

还可以使用goto语句和标签配合使用来模拟循环

package main

func main() {
    i:=0
    HERE:
        print(i)
        i++
        if i==5 {
            return
        }
        goto HERE
}

使用标签和 goto 语句是不被鼓励的

## 函数

除了 main ()、init () 函数外,其它所有类型的函数都可以有参数与返回值。函数参数、返回值以及它们的类型被统称为函数签名。

定义及调用格式如下:

func function_name( [parameter list] ) [return_types] {
  // 函数体
}
pack1.Function(arg1, arg2, , argn)  //  Function 是 pack1 包里面的一个函数

例子:

func Test(a,b int) int { //第一行,参数,返回值类型
  /* Your code */
  return a
}

Go 语言函数可以返回多个值

func div(a,b int) (int int) {  // 返回值类型也可以写成 (q,r int) 的形式,直接 return 会默认返回函数内的 q,r 的值
  return a/b, a%b
}
q, r := div(13,3) // 接受返回值

函数式编程

函数式编程,函数的参数也可以是一个函数

// 这段代码接收3个参数,其中第一个参数 op 为函数,也可以直接写成一个匿名函数
func apply(op func(int int) int, a, b int) int {
	return op(a,b)
}

可变参数列表

func sum(numbers ...int) int {
	s := 0
	for i := range numbers {
		s += numbers[i]
	}
}
sum(1,2,3,4,5) // 调用函数,可以传入不限制数量的参数,到函数内部类似与数组

defer 和追踪

关键字 defer 允许我们推迟到函数返回之前(或任意位置执行 return 语句之后)一刻才执行某个语句或函数

func function1() {
	fmt.Printf("In function1 at the top\n")
	defer function2()  // 最后才执行 function2()
	fmt.Printf("In function1 at the bottom!\n")
}

func function2() {
	fmt.Printf("function2: Deferred until the end of the calling function!")
}

当有多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出)

func f() {
    for i := 0; i < 5; i++ {
        defer fmt.Printf("%d ", i)
    }
}

使用 defer 语句实现代码追踪

一个基础但十分实用的实现代码执行追踪的方案就是在进入和离开某个函数打印相关的消息,即可以提炼为下面两个函数:

package main

import "fmt"

func trace(s string)   { fmt.Println("entering:", s) }
func untrace(s string) { fmt.Println("leaving:", s) }

func a() {
    trace("a")
    defer untrace("a")
    fmt.Println("in a")
}

func b() {
    trace("b")
    defer untrace("b")
    fmt.Println("in b")
    a()
}

func main() {
    b()
}

闭包

当我们不希望给函数起名字的时候,可以使用匿名函数:

func(x,y int) int {return x+y}

这样的函数不能独立存在,但可以被赋值与某个变量,即保存函数的地址到变量中。然后通过变量名对函数进行调用。

匿名函数同样被称之为闭包(函数式语言的术语):它们被允许调用定义在其它环境下的变量。

计算函数执行时间

可以使用time包中的NowSub函数

start := time.Now()
longCalculation()
end := time.Now()
delta := end.Sub(start)
fmt.Printf("longCalculation took this amount of time: %s\n", delta)

通过内存缓存来提升性能

当在进行大量的计算时,提升性能最直接有效的一种方式就是避免重复计算。通过在内存中缓存和重复利用相同计算的结果,称之为内存缓存。

最明显的例子就是生成斐波那契数列的程序(详见第 6.6 和 6.11 节):

要计算数列中第 n 个数字,需要先得到之前两个数的值,但很明显绝大多数情况下前两个数的值都是已经计算过的。即每个更后面的数都是基于之前计算结果的重复计算。

而我们要做就是将第 n 个数的值存在数组中索引为 n 的位置,然后在数组中查找是否已经计算过,如果没有找到,则再进行计算。

内存缓存的技术在使用计算成本相对昂贵的函数时非常有用(不仅限于例子中的递归),譬如大量进行相同参数的运算。这种技术还可以应用于纯函数中,即相同输入必定获得相同输出的函数。

指针

Go 语言的指针相对于 C 语言更加简单

var a int = 2
var pa *int = &a
*pa = 3
fmt.Println(a)  // 输出 3

Go 语言指针不能运算

参数传递

Go 语言只有值传递,没有引用传递,通过指针的使用可以实现引用传递。

func swap(a,b *int) {
  *b,*a = *a,*b
}

内建容器

数组

数组定义

var variable_name [SIZE] variable_type

var arr1 [5]int // 定义一个数组,元素初始值为 0
arr2 := [3]int{1,3,5} // 可以直接 := ,但需要给出初始值
arr3 := [...]int{2,4,5,6}  // 自动决定长度
var grid [4][5]int // 二维数组

数组可以在声明时初始化

var array = [5]string{ "go", "java", "c++" }

数组赋值

var array = [10] string // 定义一个 10 长度的 string 类型数组

// 数组赋值
array[0] = "go"
array[1] = "java"
array[2] = "c++"

遍历数组

for i:=0;i<len(arr3);i++{
  fmt.Println(arr3[i])
}

更常用的是range,使用range意义明确、美观

for i := range arr3 {  // range 可以获得数组元素的下标
  fmt.Println(arr3[i])
}

for i,v := range arr3 {  // range 也可以直接获得元素值
  fmt.Println(i,v)
}

我们可以通过_来省略变量,只获得元素值

for _,v := range arr3 {  // 只获得值
  fmt.Println(v)
}

数组是值类型

[10]int[20]int是不同类型

函数传入数组时,仍然是值传递,不会改变原数组,但通过指针可以实现引用传递数组

切片(slice)

一种类似“动态数组”结构的数据类型

从数组中创建slice

arr := [...]int {0,1,2,3,4,5,6,7}
arr[2:6]  // 2,3,4,5

使用Slice可以实现直接对数组的操作,Slice本身没有数据,是对底层 array 的一个 view

arr := [...]int {0,1,2,3,4,5,6,7}
s := arr[2:6]
s[0] = 10 // arr 变为 [0,1,10,3,4,5,6,7]

Reslice

s := arr[2:6]
s = s[:3]
s = s[1:]
s = arr[:]

Slice 的扩展

arr := [...]int {0,1,2,3,4,5,6,7}
s1 := arr[2:6]  // [2,3,4,5]
s2 := s1[3:5]  // [5,6]

截屏2020-03-14下午6.55.35

Slice可以向后扩展,不可以向前扩展,向后不能超过底层数组的长度 cap(s)

向 Slice 添加元素

s2 := append(s1,10) // 向s1添加元素10,会修改底层数组,除非添加元素超越cap

添加元素时如果超越cap,系统会重新分配更大的底层数组

由于值传递的关系,必须接收append的返回值

s = append(s,val)

创建 Slice

前面使用了array创建Slice,也可以直接创建Slice

申明语法:var identifier []type

与申明数组不同,申明数组时需要指定长度

var s []int
s1 := []int{2,4,6,8}
s2 := make([]int,16) // 指定长度的 Slice
s3 := make([]int,1032) // 指定 Slice 的长度和底层数组(cap)的长度

Copying Slice

copy(s2,s1) // s1 拷贝到 s2

删除元素

s2 = append(s2[:3],s2[4:]...) // 删除下标为 3 的元素

Map

map是一种无序的键值对的集合

创建map

语法: var map_variable map[key_data_type]value_data_type,默认mapnil

m := map[string]string {   // map[key type]value type
  "name":"Loner"
  "gendle":"male"
}

使用map[k]v来定义map,也可以定义复合mapmap[k1]map[k2]v

也可以通过make来建立map

m2 := make(map[string]int) // 定义空map,m2 == empty map
var m3 map[string]int // m3 == nil

map一定要先进行初始再赋值,否则会创建一个 nil map,不能用来存放键值对

map 可以根据新增的key-value动态伸缩,因此不存在固定长度或者最大限制。但也可以选择表明 map的初始容量 capacitymake(map[keytype]valuetype, capacity)

dataMap := make(map[string]string, 20)  // 初始容量为 20

map 遍历

与数组遍历类似,map 内的元素是无序的,如需顺序,需要手动排序

for k,v := range m {
  fmt.Println(k,v)
}

访问元素

name := m["name"]

当访问的 key 不存在时,会返回 ZeroValue

通过接收返回值可以判断 key 是否存在

name,ok := m["nama"]  // 元素存在,则 ok 接收到 true,反之为 false

删除元素

delete(m,"name")

检测某个元素是否存在

elem, ok = m[key]

如果keym中,ok为 true; 否则为 false,并且 elem 是 map 的元素类型的零值。

map 的 key

map 使用哈希表,必须可以比较相等,除了 slice、map、function 的内建类型都可以作为 key,Struct 类型不包含上述字段,也可以作为 key

例题:寻找最长不含有重复字符的子串

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:

输入: “abcabcbb” 输出: 3 解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。 示例 2:

输入: “bbbbb” 输出: 1 解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。 示例 3:

输入: “pwwkew” 输出: 3 解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。 请注意,你的答案必须是 子串 的长度,”pwke” 是一个子序列,不是子串。

来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/longest-substring-without-repeating-characters

func lengthOfLongestSubstring(s string) int {
	last := make(map[rune]int)
	start := 0
	maxLength := 0
	for i, ch := range s {
		if i, ok := last[ch]; ok && last[ch] >= start {
			start = i + 1
		}
		if i-start+1 > maxLength {
			maxLength = i - start + 1
		}
		last[ch] = i
	}
	return maxLength
}

字符串处理

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

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

该类字符串使用双引号括起来,其中的相关的转义字符将被替换,这些转义字符包括:

\n:换行符 \r:回车符 \t:tab 键 \u 或 \U:Unicode 字符 \:反斜杠自身

该类字符串使用反引号括起来,支持换行,例如:

`This is a raw string \n` 中的 `\n\` 会被原样输出。

字符串遍历

decode 的方式来遍历,对非英文语言有奇效

for i,ch := range []rune(s) {  // 使用 []byte 可以获得所有字节
  fmt.Printf("(%d %c)",i,ch)  // 以实际的字符输出,而非编码形式
}
utf8.RuneCountInString(s)  // 可以获得字符数量
len(s) // 获得字节长度

使“寻找最长不含有重复字符的子串”支持中文

func lengthOfLongestSubstring(s string) int {
	last := make(map[rune]int)
	start := 0
	maxLength := 0
  for i, ch := range []rune(s) {
		if i, ok := last[ch]; ok && last[ch] >= start {
			start = i + 1
		}
		if i-start+1 > maxLength {
			maxLength = i - start + 1
		}
		last[ch] = i
	}
	return maxLength
}

strings 和 strconv

strings.HasPrefix(s, prefix)  //  判断 s 是否以 prefix 开头,返回 bool
strings.HasSuffix(s, suffix)  //  判断 s 是否以 suffix 结尾,返回 bool
strings.Contains(s, substr)  //  判断 s 是否包含
strings.Index(s,str)  // 返回 str 在 s 中的索引(str 的第一个字符的索引),类型为 int,-1 表示不存在
strings.LastIndex(s,str) //  返回 str 在 s 中出现最后位置的索引(str 的第一个字符的索引),类型为 int,-1 表示不存在
strings.IndexRune(s,r)  //  非 ASCII 字符,建议使用这一函数定位
strings.Replace(str,old,new,n)  //  替换 str 中的前 n 个字符串 old 为 new,并返回一个新的字符串,n=-1 则替换所有
strings.Count(s,str)  //  计算字符串 str 在 s 中揣想了的非重叠次数,返回 int
strings.Repeat(s,count)  //  重复 count 次字符串 s 并返回一个新的字符串
strings.ToLower(s)  //  转小写
strings.ToUpper(s)  //  转大写
strings.TrimSpaces(s)  //  剔除字符串开头和结尾的空白符号
strings.Trim(s,"cut")  //  剔除指定字符,TrimLeft 或者 TrimRight,剔除开头或者结尾的字符串
strings.Fields(s)  //  利用空白符号来作为动态长度的分隔符将字符串分割成若干小块,返回一个 slice
strings.Split(s,sep)  //  自定义分割符号进行分割,返回 slice
strings.Join(sl,sep)  //  将元素类型为 string 的 slice 使用分割符号来拼接组成一个字符串
strings.NewReader(str)  //  生成一个 Reader 并读取字符串中的内容,然后返回指向该 Reader 的指针,从其它类型读取内容的函数还有:Read() 从 [] byte 中读取内容,ReadByte() 和 ReadRune() 从字符串中读取下一个 byte 或者 rune
strconv.Itoa(i)  //  返回数字 i 所表示的字符串类型的十进制数

时间和日期

time包提供了一个数据类型time.Time以及示和测量时间和日期的功能函数。

t := time.Now()  //  获取当前时间
t.Day()  // 获取日
t.Minute()  //  获取分钟,类似的方法可以获取日期的一部分
t.Format("02 Jan 2006 15:04")  // 格式化日期,因先初始化 t ,var t time.Time

面向对象

结构体

结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。

Go 语言仅支持封装,不支持继承和多态

结构的定义

type TreeNode struct {
  Left, Right *TreeNode
  Value int
}

Go 语言的结构体,不论地址还是结构本身,一律使用.来访问成员

root := TreeNode{value: 3}
root.Left = &TreeNode{}
root.Right = &TreeNode{nil,nil,5}
root.Right.Left = new(TreeNode)

结构的创建

t := new(T): 变量 t 是一个指向 T的指针,此时结构体字段的值是它们所属类型的零值

person := new(Person)

t := &T{}:&T{} 这种语法叫做 混合字面量语法(composite literal syntax), 类似 &struct1{a, b, c} 这种混合字面量语法是一种简写,底层仍然会调用 new (),这里值的顺序必须按照字段顺序来写。

person := &Person {name : "xiaozhuanlan", age : 12}

使用自定义工厂函数:

func createTreeNode(value int) *TreeNode{
  return &TreeNode{Value: value}
}
root.Left.Right = createTreeNode(2)

注意返回了局部变量地址,但外部仍可以使用。编译器会自动选择堆或栈来存放局部变量。

访问结构体成员

// 结构体.成员名
person.name
person.age

带标签的结构体

结构体中的字段除了有名字和类型外,还可以有一个可选的标签(tag):它是一个附属于字段的字符串,可以是文档或其他的重要标记。

type TagType struct { // tags
    field1 bool   "An important answer"
    field2 string "The name of the thing"
    field3 int    "How much there are"
}

标签的内容不可以在一般的编程中使用,只有包 reflect 能获取它

ttType := reflect.TypeOf(tt)   //  tt 是一个结构体实例
ixField := ttType.Field(ix)
fmt.Printf("%v\n", ixField.Tag)

匿名字段和内嵌结构体

结构体可以包含一个或多个 匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时类型就是字段的名字。匿名字段本身可以是一个结构体类型,即 结构体可以包含内嵌结构体

type innerS struct {
    in1 int
    in2 int
}

type outerS struct {
    b    int
    c    float32
    int  // anonymous field
    innerS //anonymous field
}

内嵌结构体:

type A struct {
    ax, ay int
}

type B struct {
    A
    bx, by float32
}

方法

go 语言还有一种特殊的函数,叫做方法。一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。

语法格式如下:

func (variable_name variable_data_type) function_name() [return_type]{
   /* 函数体*/
}

例如:

type person struct {
	name string
	age  int
}

func (p person) GetName() string {
	return p.name
}

func (p *person) SetName() {   // 传入指针,可以改变值,引用传递
	p.name = "changeName"
}

方法是一种特殊的函数,不允许重载,但如果基于接收者类型,可以重载,具有同样名字的方法可以在 2 个或多个不同的接收者类型上存在,比如在同一个包里这么做是允许的:

func (a *denseMatrix) Add(b Matrix) Matrix
func (a *sparseMatrix) Add(b Matrix) Matrix

为结构定义方法

一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值

func (node TreeNode) print() {
  fmt.Print(node.value)
}
root.print()  // 调用方法

方法仍然是值传递,只有使用才能改变结构的内容。

nil指针也可以调用方法

func (node TreeNode) print() {
  if node == nil {
    fmt.Println("Setting value tp nil")
  }
  fmt.Print(node.value)
}

值接收者和指针接收者

封装

每个 Go 文件都属于且仅属于一个包。一个包可以由许多以 .go 为扩展名的源文件组成,因此文件名和包名一般来说都是不相同的。

必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main

### 扩展已有类型

扩充系统类型或者别人的类型

### GOPATH 环境变量

官方推荐:所有项目和第三方库都放在一个 GOPATH 下

也可以将每个项目放在不同的 GOPATH

面向接口

Go 语言不是一种 “传统” 的面向对象编程语言:它里面没有类和继承的概念。

但是 Go 语言里有非常灵活的 接口 概念,通过它可以实现很多面向对象的特性。接口提供了一种方式来 说明 对象的行为:如果谁能搞定这件事,它就可以用在这儿。

接口

duck typing

大黄鸭是鸭子吗?

从生物学上看并不是鸭子,但它像鸭子,我们就可以叫他鸭子。

duck typing:描述事物的外部行为而非内部结构

严格说 go 属于结构化类型系统,类似 duck typing

go 语言的 duck typing 同时具有 python、c++ 的 duck typing 的灵活性,又具有 java 的类型检查。

接口的定义和实现

接口是一种类型,一种抽象的类型

接口定义了一组方法(方法集),但是这些方法不包含(实现)代码:它们没有被实现(它们是抽象的)。接口里也不能包含变量。—— Go 入门指南

接口类型是由一组方法定义的集合。—— Go 语言之旅

Go中的接口为指定对象的行为提供了一种方法:如果某样东西可以完成这个, 那么它就可以用在这里。 —— 实效 GO 编程

接口由使用者定义,不同于面向对象

接口的定义格式:

type Namer interface {
    Method1(param_list) return_type
    Method2(param_list) return_type
    ...
}

Namer是接口类型

(按照约定,只包含一个方法的)接口的名字由方法名加 er 后缀组成,例如 Printer、Reader、Writer、Logger、Converter 等等。还有一些不常用的方式(当后缀 er 不合适时),比如 Recoverable,此时接口名以 able 结尾,或者以 I 开头

type Retriever interface {
  Get(source string) string
}
func download(retriever Retriever) string {
  return retriever.Get("www.imooc.com")
}

接口的实现是隐式的,只要实现接口里的方法。

接口变量

接口的组合

接口可以组合使用

type interface1 interface{}
type interface2 interface{}
type interface3 interface{
  interface1
  interface2
}

常用系统接口

Stringer、Reader/Writer

反射

反射可以在运行时检查类型和变量,例如它的大小、方法和动态的调用这些方法。

这对于没有源代码的包尤其有用。这是一个强大的工具,除非真得有必要,否则应当避免使用或小心使用。

获取某个变量的类型:

reflect.TypeOf(x)

获取变量值:

reflect.TypeOf(x)