Go编程

37 阅读22分钟

什么是 Go

Go 是 Google 开发的一种编程语言。 它于 2009 年由 Robert Griesemer、Rob Pike 和 Ken Thompson 作为开源项目发布。

Go 语言与 C 语言有很多相似之处,它继承了 C 语言语法的许多方面,如控制流语句、基本数据类型、指针和其他元素等。 不过,该语言的语法和语义均超出 C 语言。 它还与 Java、C#、Python 等有相似之处。 一般情况下,Go 语言往往从其他编程语言中借用并调整功能,同时去掉了大部分复杂性。 例如,可以在 Go 语言中使用一些面向对象的 (OO) 编程功能和设计模式,但并不完全实现整个 OO 范例。

win 环境中安装 Go

All releases - The Go Programming Language 处下载 Go 的安装程序,或者 zip 包;如果是 zip 包,需要手动解压,并在环境变量中配置对应的环境变量,如下

# go bin 目录的上一级,即 go 的安装路径
GOROOT
# go 的工作区目录
GOPATH

关于 Go 的工作区,即源代码存放目录,所有 Go 项目共享同一个工作区,工作区中还包含三个文件夹

  • bin:包含应用程序中的可执行文件。
  • src:包括位于工作站中的所有应用程序源代码。
  • pkg:包含可用库的已编译版本。 编译器可以链接这些库,而无需重新编译它们。

工作区中的 src 即源代码的存放目录,目录中又划分各个子项目目录如下结构

src/

​ projectDir1/

​ prijectDir2/

配置国内镜像源

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

变量声明

声明变量,指定类型为字符串

var firstName string 

声明多个变量,统一指定类型为字符串

var firstName, lastName string

使用括号初始化多个变量,类型信息可以省略,go 会根据具体值推断变量类型

var (
    firstName string = "zhang"
    lastName  string = "san"
    age       int    = 18
)

单行声明和初始化多个变量(按顺序)

var firstName, lastName, age = "zhang", "san", 18
println(firstName, lastName, age)

使用 := 声明函数内的局部变量,相应的,函数外必须使用 var 声明变量

:= 只能用于声明新变量,重复声明将无法编译

fistName, lastName := "zhang", "san"

注意:变量声明但未使用,会导致编译不通过

常量声明

声明常量

const HTTPStatusOK = 200

使用 () 声明多个常量。使用 iota 关键字可以方便的定义常量值,因为 iota 的值会依次递增

const (
    SPRING = iota // 0
    SUMMER 	      // 1
    AUTUMN 		  // 2
    WINTER 		  // 3
)

// 但在下个 const 区域,iota 的值会重置为 0
const (
    A = iota // 0
    B // 1
)

// 官方对应 iota 的示例
type ByteSize float64
const (
    _           = iota             // 初始值为 0
    KB ByteSize = 1 << (10 * iota) // 1 << (10 * 1) iota 值为 1,往下依次递增
    MB
    GB
    TB
    PB
    EB
    ZB
    YB
)

注意:常量可以申明但不使用

数据类型

Go中主要有四种数据类型

  • 基本类型:数字、字符串和布尔值
  • 聚合类型:数组和结构
  • 引用类型:指针、切片、映射、函数和通道
  • 接口类型:接口

基本数据类型

整型

var integer8 int8 = 127
var integer16 int16 = 32767
var integer32 int32 = 2147483647
var integer64 int64 = 9223372036854775807
fmt.Println(integer8, integer16, integer32, integer64)

// 不同类型不能直接运算
var ops1 int16 = 127
var ops2 int32 = 32767
println(ops1 + ops2)

浮点型

var f32 float32 = 2147483647
var f64 float64 = 9223372036854775807
fmt.Println(f32, f64)

// 查看最大浮点值
println(math.MaxFloat64, math.MaxFloat32)

布尔类型

// 布尔类型
var is = false

字符串型

// 字符串
var name = "zhangsan"

类型转换

// 类型转换
var i32 int32 = 10
var i64 int64 = 20
println(int64(i32) + i64)

// 使用 strconv 将字符串转换为 int
result, _ := strconv.Atoi("-42")
println(result)

如果不对变量进行初始化,将具备默认值

  • int 类型的 0(及其所有子类型,如 int64
  • float32float64 类型的 +0.000000e+000
  • bool 类型的 false
  • string 类型的空值

聚合数据类型

数组

在 Go 中,数组是一种特定类型且长度固定的数据结构。它们可具有零个或多个元素且必须在声明或初始化时定义大小。此外,数组一旦创建,就无法调整大小。

使用如下形式定义一个 int 数组,长度为 3,Go会使用对应类型的默认值初始化数组

var arr [3]int

// 通过索引为数组赋值
for i := 0; i < len(arr); i++ {
    arr[i] = i
}
fmt.Println(arr)

创建并初始化数组

var languages = []string{"Java", "Go", "JavaScript", "C", "C++"}

多维数组

var arr [3][3]int
for i := 0; i < len(arr); i++ {
    for j := 0; j < len(arr[i]); j++ {
        arr[i][j] = rand.Int()
    }
}

切片

切片可以理解为基础数组的一个子集,一个片段,是一个拥有相同类型元素的可变长度的序列;它是基于数组类型做的一层封装,非常灵活,支持自动扩容。

切片的创建方式如下

// 创建一个空切片
var slice1 []string
// 创建并初始化一个切片
var slice2 = []string{"1", "2", "3", "4", "5", "6", "7"}
// 使用 make 函数创建一个 int 类型,长度为 3,容量为 5 的切片
slice3 := make([]int, 3, 5)

还可以通过切片运算符 s[ startIndex : endIndex ]( 左闭右开) 从数组或切片来创建新的切片

// 通过数组创建新切片
var arr = []int{1, 2, 3, 4, 5}
var slice = arr[0:2]
// 通过切片创建新切片
slice2 := slice[0:1]

通过切片运算符创建的切片:切片的长度等于切片的 endIndex - startIndex,切片的容量等于 底层数组的长度 - startIndex,分别可以使用len(slice) 和 cap(slice)函数计算出

需要注意的是,当通过一个切片创建出另一个新切片时,底层使用的是同一个数组,修改一个切片的元素也会影响另一个切片

slice1 := []int{1, 2, 3, 4, 5}
slice2 := slice1[0:2]
slice2[1] = 4
fmt.Println(slice1) // [1 4 3 4 5]
fmt.Println(slice2) // [1 4]

访问切片元素可以直接通过索引,如slice[0] slice[1],索引从 0 开始,最大索引值为len(slice) - 1

使用内置的 append() 函数可以往切片中追加元素,当容量不够时,会自动扩容,如下

var slice []int
slice2 := append(slice, 1, 2, 3)

遍历元素

var slice []int
slice2 := append(slice, 1, 2, 3)

for ele := range slice2 {
    fmt.Println(ele)
}
// 或者
for i := 0; i < len(slice2); i++ {
    fmt.Println(slice2[i])
}

切片的复制

// 将 slice1 中的元素复制到 slice2 中
slice1 := []int{1, 2, 3}
var slice2 = make([]int, 3)
copy(slice2, slice1)

排序

slice := []int{11, 18, 4, 5, 2, 1}
sort.Ints(slice) // int 排序
fmt.Println(slice) // 默认降序

相关文章 Go语言基础结构 —— Slice(切片) - 知乎 (zhihu.com)

映射

Go 中的映射由哈希表实现,用于存储键值对的数据集合,映射中的键必须是同一类型,值也必须是同一类型。

申明和初始化一个映射

// 声明 map,键值分别为字符串和整型
var userAges = map[string]int{
    "zhangsan": 18,
    "lisi":     20,
    "wangwu":   21,
}
// 也可使用 make() 函数创建一个空映射
m := make(map[string]int)

添加项

m["zhangsan"] = 18
m["lisi"] = 20

只申明但未初始化的 map,不能直接添加项,这会导致 panic

var nilMap map[string]int
nilMap["age"] = 21

使用[key]可以访问映射中的项,若项不存在,不会执行 panic,会获得对应类型的默认值

m := map[string]string{
    "zhangsan": "AA",
}
fmt.Println(m["lisi"])

如果想了解对应的项是否存在,可以使用映射的下标表示法第二个参数,如下

m := map[string]string{
    "zhangsan": "AA",
}
val, exist := m["lisi"]
if exist {
    fmt.Println(val)
} else {
    fmt.Println("key 不存在")
}

使用内置的 delete 函数删除映射中的项(删除不存在的项也不会产生panic),如下代码

m := map[int]string{
    1: "zhangsan",
    2: "lisi",
    3: "wangwu",
}
delete(m, 1)
fmt.Println(m)

遍历映射

m := map[int]string{
    1: "zhangsan",
    2: "lisi",
    3: "wangwu",
}
// 遍历键值对
for k, v := range m {
    fmt.Println(k, "==", v)
}

// 忽略键或值
for _, v := range m {
    fmt.Println(v)
}

// 默认遍历键
for k := range m {
    fmt.Println(k)
}

结构

Go 中使用结构体来表示多个字段的集合,结构体的定义和基本使用如下

// 定义一个 User 类型的变量
var user User
// 初始化 User 类型的结构体
user = User{
    id:   1,
    name: "zhangsan",
}
// 可直接访问结构体中的字段
fmt.Println(user.id)
// 修改结构体中字段的值
user.name = "lisi"

使用指针指向结构体

user = User{
    id:   1,
    name: "zhangsan",
}
// 获取 user 结构体的指针
user2 := &user
user2.id = 2 // 在 user2 上的修改会影响原本的 user

结构体的字段类型,也可以声明为结构体,称之为结构嵌入,如下

type Dept struct {
    id       int
    deptName string
}
type User struct {
    id   int
    name string
    dept Dept
}

user := User{
    id:   1,
    name: "zhangsan",
    dept: Dept{
        id:       1,
        deptName: "研发部",
    },
}

fmt.Println(user.dept.deptName) // 访问嵌套结构体中的字段

使用 JSON 解码和编码结构体

type User struct {
    Id   int // 字段首字母需要大写,代表可访问的,否则 Marshal() 函数将访问不到对应的字段
    Name string
}
user := User{
    Id:   1,
    Name: "zhangsan",
}
marshal, err := json.Marshal(user)
if err == nil {
    // 将结构体转换为 Json 字符串
    fmt.Printf("%s\n", marshal)
}

// 解码 json 字符串,转换为结构体
var parseUser User
err = json.Unmarshal(jsonStr, &parseUser)
if err == nil {
    fmt.Println(parseUser)
}

函数

Go 中使用 func 关键字声明函数,使用函数可以将一组逻辑进行抽取,便于代码的复用和维护

对于 main 函数,只能有一个,其作为可执行程序运行的起点,main 函数不接受任何参数,使用 os.Args[0]os.Args[1] 等方式取得命令行参数

创建自定义求和函数

var sum = func(ops1 int, ops2 int) int {
    return ops1 + ops2
}

创建自定义计算函数,返回多个值

var calc = func(ops1 int, ops2 int) (sum int, mul int) {
    sum = ops1 + ops2
    mul = ops1 * ops2
    return sum, mul
}
var sumResult, mul = calc(1, 4) // 接收多个返回值
var sumResult2, _ = calc(1, 4) // 可以使用 _ 忽略一个的值
println(sumResult, mul)
println(sumResult2)

Go中函数的参数使用值传递,若想更改函数参数的值,需要借助指针

var rawStr = "zhangsan"
// 函数参数使用 * 声明为字符串类型的指针
var updateStr = func(strP *string) {
    *strP = "Hello" // 使用 * 调用指针赋值为 Hello,这将直接修改对应地址的内容
}
updateStr(&rawStr)
println(rawStr)

Go中的包和其他编程语言的依赖和模块类似,可以打包代码,发布包,其他项目可以依赖包,便于代码复用;默认包为 main 包,若程序为 main 包的一部分,打包时会生成对应的二进制可执行文件,否则将会生成存档文件,具有 .a 的扩展名

在 Go 中,包使用其导入路径的最后一部分作为名称,例如导入名称为 "math/cmplx",则使用时应该通过 cmplx 调用

创建包

在 Go 工作空间中,创建目录作为包目录,在其中创建 sum.go 文件,如下内容

package calculator // 指定包名称,一般为包目录名称

const logMessage = "[LOG]"

// Version of the calculator
const Version = "1.0"

func internalSum(number int) int {
	return number - 1
}

// Sum two integer numbers
func Sum(number1, number2 int) int {
	return number1 + number2
}

一些约定如下

  • 如需将某些内容设为专用内容,请以小写字母开始。
  • 如需将某些内容设为公共内容,请以大写字母开始。

在此包目录下执行 go build 可确认一切正常,但不会生成二进制文件,因为其不在 main 包中。

使用如下命令创建模块(如果使用 GoLand IDE 开发,会自动创建)

go mod init calculator

这样就创建了一个本地包,如果要在其他程序中引入,需要使用当前包对应的模块名称进行引入。

包创建后会生成 mod 文件,内容如下

module calculator // 包名

go 1.20 // go版本

引入包

上面已经创建了一个名为 calculator 的包,如果要在当前项目中引入,首先确保当前项目已经初始化了模块,也就是执行

go mod init <mode-name>

之后在 mod 文件引入上面的 calculator 包,如下

require calculator v0.0.0

// 将申明的 calculator 依赖指向本地依赖,否则会从网络加载依赖、
// 如果本地包和当前项目都在 Go 工作区中,那么包路径只能为 ../xxx
replace calculator => ../calculator

如果引入的包中还有其他关联的包,可以尝试执行如下命令

# go tidy 命令会下载关联的包,同时 tidy 还会删除多余依赖
go tidy

最后在当前项目中使用 import calculator 引入即可。

控制流

if else

与大多数语言一样,在 Go 中,依然使用 if else 作为条件控制语句且 if 条件无需使用括号;值得注意的是,Go 中没有三元运算,条件判断必须编写完整的 if 语句

一个基本的 if 示例如下

// 判断随机数是否为偶数
if rand.Int()%2 == 0 {
    println("Yes")
} else {
    println("No")
}

复合 if 语句

var getNumber = func() int {
    return -1
}

// 复合 if 语句,将变量 x 的赋值和判断写在一行
// 注意:变量 x 只在 if 及其分支代码块中有效
if x := getNumber(); x < 0 {
    println("<0")
} else if x > 0 {
    println(">0")
}

switch

一个基本的 switch 示例如下

rom := rand.Int() % 2
switch rom {
case 0:
    println("OK")
case 1:
    println("NO")
default:
    println("UN")
}

使用多个表达式

// case 可包含多个选项,使用 , 分隔
language := "Java"
switch language {
case "Java", "Go", "JavaScript", "C":
    println("Yes!!")
case "Python":
    println("No!!")
}

switch 函数调用

switch time.Now().Weekday().String() {
case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday":
    fmt.Println("It's time to learn some Go.")
default:
    fmt.Println("It's the weekend, time to rest!")
}

使用函数调用的好处在于,无需修改 switch 表达式即可调整判断逻辑,只需要调整函数内部逻辑即可

省略条件

// 省略 switch 中的条件语句,直接在 case 中判定
switch {
case rand.Int()%2 == 0:
    println("ZERO")
default:
    println("NO ZERO")
}

手动进入下一个 case

// 进入到下一个 case,go中的 switch 在匹配到一个 case 后,不会进入下一个 case
// 这也意味着无需手动 break,要想继续进入下一个 case,可以使用 fallthrough 关键字,如下、
switch {
case rand.Int()%2 == 0:
    println("YES ZERO")
    fallthrough
case rand.Int()%2 == 1:
    println("YES ONE")
default:
    println("UNKNOWN")
}

for

同 if 一样,Go 中的 for 循环也无需大括号,使用 ; 分隔 for 的三个部分

  • 在第一次迭代之前执行的初始语句(可选)。
  • 在每次迭代之前计算的条件表达式。 该条件为 false 时,循环会停止。
  • 在每次迭代结束时执行的后处理语句(可选)。

最简单的 for 循环示例如下

sum := 0
for i := 1; i <= 100; i++ {
    sum += i
}
fmt.Println("sum of 1..100 is", sum)

值得注意的时,Go 中没有 while 语句

空的预留处理器和后处理器

// 正因为没有 while 语句,可用 for 替代,只需要指定结束条件即可
for num != 5 {
    num = rand.Int63n(15)
    fmt.Println(num)
}
// 同样,结束条件留空,则相当于 while(true) 形式
for {
    println(rand.Int())
}

退出循环

// 使用 break 退出无限循环
var breakCondition = 0
for {
    if breakCondition = rand.Int(); breakCondition%2 == 0 {
        break
    }
    breakCondition = rand.Int()
}
println(breakCondition)

跳过循环

// 使用 continue 跳过当次循环
var count = 1
var sum = 0
for count <= 100 {
    if count%5 == 0 {
        count++
        continue
    }
    sum += count
    count++
}
println(sum)

defer panic recover

defer、panic、recover 是 Go 特有的控制流函数

defer

defer 语句会推迟任何函数的执行,知道包含 defer 语句的函数执行完成,通常用于定义一个避免忘记完成的任务(例如关闭文件流),如下

// 创建文件
newfile, err := os.Create("learnGo.txt")
if err != nil {
    fmt.Println("Error: Could not create file.")
    return
}

// 使用 defer 定义关闭流的操作,这将在当前函数执行完后执行
defer newfile.Close()

// 写入文件
if _, err = io.WriteString(newfile, "Learning Go!"); err != nil {
    println("无法写入文件,", err)
    return
}
// 刷新磁盘(持久化)
err = newfile.Sync()
if err != nil {
    println("文件写入失败", err)
}

panic

panic 代表运行时异常,这将使 Go 程序崩溃以退出正常的控制流,但这并不影响 derfer 定义的延迟函数的执行,简单来说,即使出现异常情况,也可以保证资源的关闭等后续操作,如下

// i 不是偶数时,抛出异常信息;即使抛出异常,借助 defer 最终也能打印 i 的值
i := rand.Int()
defer println(i)
if i%2 != 0 {
    panic("i 不是一个偶数")
}

recover

recover:恢复,顾名思义,recover 函数能够在程序抛出 panic 后,恢复程序的控制器以便继续执行(recover 需要在 defer 函数中调用),如果程序正常运行的情况下调用 recover,则返回 nil,如下示例

var processorNumber = func(number int) {
    defer println(number)
    if number%2 != 0 {
        panic("number 不是一个偶数")
    }
}

number := rand.Int()
processorNumber(number)
println("执行结束") // 在没有使用 recover 的情况下,processorNumber 函数如果产生 panic,则不会执行到此行

修改后如下

var processorNumber = func(number int) {
    defer func() {
        // 在 defer 标识的匿名函数中,调用 recover 方法,如果没有返回 nil,则说明运行过程中有异常产生
        // 这可以用于定义一些只在异常情况下才需要执行的逻辑
        // 同时,调用 recover 后,控制台也不会出现异常信息
    	if recover() != nil {
    		println(number, "panic,继续执行")
    	}
    }()
    if number%2 != 0 {
        panic("number 不是一个偶数")
    }
}

number := rand.Int()
processorNumber(number)
println("执行结束") // recover 的调用也保证即使 processorNumber 执行 panic,程序运行能够恢复,继续执行到此行

错误处理

Go 中原生的错误处理是一种只需要借助 if 和 return 的控制流机制,如下示例

type User struct {
    Id   int
    name string
}
// 使用两个返回值,一个是实际的返回数据,一个是返回的错误(一般放在最后)
// 这里返回了实际数据的指针,是因为可能出现错误的情况,数据位应该为 nil,使用指针才能匹配 nil 返回值
var getUser = func() (*User, error) {
    user := User{
        Id:   1,
        name: "zhansan",
    }
    // 正常返回,错误位置为 nil
    return &user, nil
}

var fetchUserApi = func() (*User, error) {
    user, err := getUser()
    // err 为 nil,说明 getUser() 正常返回
    if err == nil {
        return user, nil
    }
    // 否则 getUser() 执行过程中可能有错误,将错误继续 return(交给调用方检查),同时数据位使用 nil 替代
    return nil, err
}

// 针对 fetchUserApi() 的执行,只需要使用 if err != nil 来判断是否需要处理错误逻辑
user, err := fetchUserApi()
if err != nil {
    return
}
fmt.Println(user)

以上是较为常见的错误处理,将错误传递给调用方,而自身不执行其他的任何操作,如果想要在传播错误之前添加更多信息,可以使用

fmt.Errorf("error info:%v", err)

对于一些常用的错误消息,可以使用如下方式创建可复用的错误

loginErr := errors.New("login error")

日志

Go 中的原生日志实现较为简单且不支持日志级别,简单使用如下

// 打印日志,自动将日期和时间作为前缀添加
log.Print("err msg")
// 打印错误日志并结束程序,相当于 os.Exit(1)
log.Fatal("err msg")
// 打印错误日志并结束程序,同时会获得堆栈跟踪
log.Panic("err msg")
// 为日志设置指定的前缀
log.SetPrefix("userLogin()-")

Go 的几个日志框架LogruszerologzapApex

方法和接口

声明方法

方法是一种特殊的函数,方法的定义需要指定调用时具体的接收方,如下

// Stu 声明方法,需要先声明结构体
type Stu struct {
	Id   int
	Name string
}

// 声明方法,与单纯的函数声明不同,方法的声明,需要指定调用时的接收方,即对应的本地类型(同一包下定义的类型)
func (s Stu) show() {
	// 在方法中,可以拿到接收方对应类型中的字段
	fmt.Println(s.Id, s.Name)
}

// 实例化一个结构体
var stu = Stu{
	Id:   1,
	Name: "zhangsan",
}

// 通过结构体实例可以调用到绑定在其中的方法
func methodDemo() {
	stu.show()
}

需要注意的是,不能用平常的方式去调用show()方法,因为它必须指定一个调用时的接收方,可以理解为 JavaScript 中的 this;同时对于相同名称的方法,可以指定不同的结构体作为接收方,例如

func (s Stu) show() {
	fmt.Println(s.Id, s.Name)
}

func (u User) show() {
	fmt.Println(u.Id, u.Name)
}

方法指针

方法的调用接收方可以申明为指针,这样在方法内对接收方的修改将会同步到具体的接收方本身,根据 Go 的约定,如果结构的任意方法为具有指针接收方,那么结构的所有方法都必须声明为指针类型,即使某个方法不需要也必须这样声明,如下

// 声明接收方为指针类型
func (s *Stu) show() {
    // 通过指针接收方更改字段值
	s.Id = 1000
	s.Name = "lisi"
}

方法嵌套

结构体的字段可以声明为结构体,以此来复用其他结构体中的字段,这对于结构体中的方法也适用,如下

type Stu struct {
	Id   int
	Name string
}

type User struct {
	Id   int
	Name string
	Stu // Stu 类型的字段
}

// 为 Stu 类型声明方法
func (s *Stu) show() {
	s.Id = 1000
	s.Name = "lisi"
	fmt.Println(s)
}

// 创建 User 实例
var user = User{
	Id:   1000,
	Name: "wangwu",
	Stu: Stu{
		Id:   1000,
		Name: "lisi",
	},
}

// 那么在 user 类型中能够直接调用到 Stu 的方法,这称之为方法嵌套
user.show()

实际上针对嵌套方法,Go 编译器会在底层创建一个接收方为 User 的方法,在方法中间接调用 Stu 中的方法,如下

func (u User) show() {
    u.Stu.show() // 间接调用 Stu 的 show() 方法 
}

公开方法

针对不同包中的:结构体、结构体字段、变量、常量、方法、函数,都可以使用大写标识符来公开,使用小写标识符来声明为私有,如下

package count

import "fmt"

// Counter 使用大写标识,即可公开此结构体
type Counter struct {
	value int // 结构体字段也是一样
}

// Creator 公开
func Creator(value int) Counter {
	return Counter{
		value: value,
	}
}

// 私有
func (c *Counter) incr() {
	c.value += 1
}

// PlusOne 公开
func (c *Counter) PlusOne() {
	c.incr()
}

// Print 公开
func (c *Counter) Print() {
	fmt.Println(c.value)
}

// 使用
counter := count.Creator(10)
counter.PlusOne()
counter.Print()

声明接口

接口是一种用于定义其他类型行为的类型,Go 中的接口是满足隐式实现的,Go 不提供用于实现接口的关键字。一个接口的声明如下

// 定义 Person 接口,定义人的基本行为,任何实现 Person 接口的类型都应该具备这些行为
type Person interface {
	Eat()
	Sleep()
}

实现接口

Go 中无需接口实现的关键字,只要定义的类型具有接口定义的所有方法,则满足隐式实现,如下代码

// Man 自定义类型
type Man struct {
	name string
	age  int
}

// Eat 自定义类型实现 Person 接口中的 Eat 方法
func (m Man) Eat() {
	fmt.Println("eat...")
}

// Sleep 自定义类型实现 Person 接口中的 Sleep 方法
func (m Man) Sleep() {
	fmt.Println("sleep...")
}

以上,Man 结构体通过定义 Eat 和 Sleep 方法,隐式实现了 Person 接口

这时再声明另外一个 Person 接口的实现 Woman,那么在声明一个函数或方法的参数签名时,如果需要用到这两种类型,则可以统一声明为 Person,如下

// 接收 Person 类型的参数
func alive(person Person) {
    // 打印实际的类型
    fmt.Printf("%T\n", person)
	person.Eat()
	person.Sleep()
}

man := Man{
    name: "zhangsan",
    age:  18,
}
woman := Woman{
    name: "xiaohong",
    age:  19,
}
// 可以接收 Man 和 Woman 类型的实例
alive(man)
alive(woman)

针对 Print Printf Println 等打印函数,函数,可以实现 Stringer 接口来自定义打印的字符串,Stringer 接口的声明如下

/*
Stringer 由具有 String 方法的任何值实现,该方法定义该值的“本机”格式。String 方法用于打印作为操作数传递到接受字符串的任何格式或无格式打印机(如 Print)的值。
*/
type Stringer interface {
	String() string
}

为 Man 类型实现 Stringer 接口的 String() 方法,如下

func (m Man) String() string {
    // 在打印 Man 实例时,将会输出此方法的返回值
	return m.name + "==" + strconv.Itoa(m.age)
}

扩展现有接口实现

假设现在通过 api 获取到响应数据,需要定制输出到控制台的内容,可以通过实现已有的接口来扩展这一功能,一种方式是调用io.Copy将源字节数组的内容复制到目标,源和目标分别为 Reader 和 Writer 接口,如下

func Copy(dst Writer, src Reader) (written int64, err error) {
	return copyBuffer(dst, src, nil)
}

源数据由 api 响应数据中取得,我们需要定义自己的 Writer 接口实现,如下

type SimpleWriter struct {
}

func (s SimpleWriter) Write(p []byte) (n int, err error) {
	var resp []GithubResponse
	err = json.Unmarshal(p, &resp)
	if err != nil {
		log.Print("json 转换失败:", err)
		return -1, err
	}
	for _, ele := range resp {
		fmt.Println(ele.FullName)
	}
	return len(resp), nil
}

同时还需要定义自己的响应结构体

type GithubResponse struct {
	// `json:"full_name"` 用于指定 json 序列化和反序列化时的字段
	FullName string `json:"full_name"`
}

最终,将接口响应的数据,通过自定义的 Writer 接口进行输出

func GithubApiDataFetch() {
	resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
	if err != nil {
		fmt.Println("Error:", err)
		os.Exit(1)
	}
	defer resp.Body.Close()
	var buf bytes.Buffer
    // io.Copy 函数会以块的形式从响应主体读取数据,并将这些块传递给 Write 方法,因此,先将所有的响应数据累计到 buf 中
	_, err = io.Copy(&buf, resp.Body)
	if err != nil {
		fmt.Println("Error:", err)
		os.Exit(1)
	}
	var simpleWriter = SimpleWriter{}
    // 最终再将累计的 buf 通过自定义的 Writer 输出
	io.Copy(simpleWriter, bytes.NewReader(buf.Bytes()))
}

并发

goroutine