zroger's blog

Go 笔记

ZRoger ZRoger #Go

系统过一遍 Go 语言的笔记:从安装配置与程序结构,到类型、切片、map、函数、结构体、方法、接口、反射与并发。

这个系列是我系统学习 Go 语言时整理的流水账笔记。

  1. 前半部分是基础部分:安装配置与第一个 hello world、程序的基本结构、类型与变量、常量与运算符、控制语句,然后是数组、切片、map 这些内置数据结构。
  2. 后半部分进入 Go 的面向对象风格与并发模型:函数、结构体、方法、接口、反射,最后是 goroutine、channel 与 select。每篇都尽量把语法规则和可运行的示例放在一起。

Go 安装配置与 Demo

ZRoger ZRoger #Go

Go 入门第一篇:语言定位与适用场景、安装踩坑与常用命令、GOPATH 的作用,以及第一个 hello world 与命令行参数。

什么是Go ?

​ Go是一门 并发支持、垃圾回收的编译型系统编程语言,旨在创造一门具有静态编译语言的高性能和动态语言的高效开发之间拥有良好平衡点的一门编程语言。

适用场景

​ 主要是为了处理高并发和异步的场景,并简化代码,提高开发效率,提高项目运行时的稳定性。像是一些云产品处理高峰时候的并发请求,或者是一些桌面程序。Go的杀手级应用是Docker,其他是PaaS类型的项目,或是消息缓存中间件,日志收集等。可以看下这个知乎上面,这个回答:https://www.zhihu.com/question/296426314

主要特点

  • 类型安全和内存安全
  • 以非常直观和极低代价的方案实现高并发
  • 高效的垃圾回收机制
  • 快速编译(同时解决C语言中头文件太多的问题)
  • 为多核计算机提供性能提升方案
  • UTF-8 编码支持

安装时的坑

当前环境为win10,安装文件地址为:https://golang.org/dl/

下载msi文件就可以了,执行它并按照安装的指导进行安装,会为我们在PATH配置好环境,并在用户系统变量中配置GOPATH,这个变量需要作修改。还有一个GOROOT变量指向我们的Go SDK 路径(即安装Go的地址)。安装后,检验下:

Terminal window
# 查看安装的版本
go version
# 查看关于go的环境变量
go env

常用命令

  • go get : 获取远程包,需要git或hg环境

  • go run : 直接允许程序

  • go build : 测试编译,检查是否有编译错误

  • go fmt : 格式化源码

  • go install : 编译包文件并编译整个程序

  • go test : 运行测试文件

  • go doc : 查看包文档

在使用go get之前,可以修改下go proxy的地址

Terminal window
set GOPROXY = "https://goproxy.io,direct"

GOPATH的作用

在配置GO环境时,除了GOROOT与PATH外,还需要在用户变量中修改GOPATH的值,这里指向工作空间的地址。在该工作空间中创建项目才可以运行,为什么会这样呢?

GOPATH 工作空间是一个目录层次结构,其根目录包含三个子目录(这些需要自己手动创建):

  • src:包含 Go 源文件,注意:你自己创建依赖的package,也要放到GOPATH 目录下,这样才能够被引用到。
  • pkg:包含包对象,编译好的库文件
  • bin:包含可执行命令

GOPATH相对于工作区的概念,可以分为Go工作区和用户工作区,GOROOT是Go核心工作区

GOPATH可以包含多个包含 Go 语言源码文件、包文件和可执行文件的路径,而这些路径下又必须分别包含三个规定的目录:src、pkg 和 bin

程序的整体结构

bin
xxx
pkg
平台名/ 如:darwin_amd64、linux_amd64
mymath.a
xxx/
aa/
bb.a
src
xxx
xxx.go
xx2
xx2.go

Hello Go’s World

hello world

新建一个文件hello.go,内容如下:

文件名:hello.go
package main // 包、表明代码所在模块或包
import "fmt" // 引入代码依赖
func main(){
fmt.Println("Hello,World!")
}

使用go run直接运行这个文件,会直接输出:

Terminal window
Hello,World!

也可以通过go build编译这个hello.go文件,编译后产生一个静态连接且独立的二进制文件hello.exe,可以复制到不同机器上运行(容器相关)。

go build hello.go

关于程序入口

与Spring Boot相似,以application为项目入口启动程序。Go的可执行程序需要一个应用程序的入口,文件夹和文件名的名字随意(文件名一般是main.go),但是必须是main包,既package main,还有必须是main方法,既func main()。所以package的名字可以和目录名不一致。

在Java中,package的名字需要和目录一致

退出返回值

func main()是不支持返回值,我们可以一试:

func main() int {
fmt.Println("Hello,World!");
return 1 ;
}

然后编译并允许,结果如下:

Terminal window
# command-line-arguments
.\hello.go:5:6: func main must have no arguments and no return values

可以通过os来返回值:

func main() {
fmt.Println("Hello,World!");
return 1 ;
os.Exit(123);
}

结果如下:

Terminal window
Hello,World!
Process finished with exit code 123

获取命令行参数

在Java中,main一般这样写,其中String[] args用来接收一些参数:

public static void main(String[] args){
.....
}

然而在Go中不支持传入参数,func main(arg []string),这样在编译会产生错误提示。可以使用os.Args来获取命令行参数。

func main( ) {
fmt.Println("Hello,World!");
if len(os.Args)>1 {
fmt.Println("Hello,World!",os.Args[1]);
}
}

然后在终端中输入:

Terminal window
go run hello.go Zing

结果:

Terminal window
Hello,World! Zing

其中:os.Args[0]会输出hello.go二进制文件的地址。

其他

本地web文档

一般在命令中输入go doc来查找有点麻烦,资源网站访问有时加载很慢。所以可以尝试下在本地启动web文档,在一些比较老的版本中:

godoc -http=localhost:8088

在新版本中,默认不带有godoc,需要自行安装:

go get golang.org/x/tools/cmd/godoc

若遇到下载不成功的问题,考虑调整Go的代理地址。

安装成功后,在GOPATH中的bin文件夹里,在里面使用。也可以将其复制到GOROOT的bin中,这样在任意文件夹位置都可以使用。

参考资料

  1. 《The Way To Go》: https://www.cntofu.com/book/14/index.html
  2. Go-Wiki : http://code.google.com/p/go-wiki
  3. go 中文地址 : https://golang.google.cn/

Go 程序的基本结构和要素

ZRoger ZRoger #Go

Go 程序的基本结构:内置关键字、Go 包的定义与导入、可见性规则、func 函数与注释约定。

内置关键字

均为小写,关键字不能作为标识符使用

break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var

除了以上,还有一些预定义标识符

append bool byte cap close complex complex64 complex128 uint16
copy false float32 float64 imag int int8 int16 uint32
int32 int64 iota len make new nil panic uint64
print println real recover string true uint uint8 uintptr

程序的代码通过语句来实现结构化。每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾,因为这些工作都将由 Go 编译器自动完成

Go 包

定义

每个Go文件都属于且仅属于一个包,一个包可以由许多以.go为扩展名的源文件组成,因此文件名和包名一般来说都是不相同的。必须在源文件中非注释的第一行中指明该文件属于哪个包,package main。package main表示一个可独立执行的程序,每个Go应用程序都包含一个名为main的包。所有的包名应该都是小写

如果想要构建一个程序,则包和包内的文件都必须以正确的顺序进行编译。包的依赖关系决定了其构建顺序。

属于同一个包的源文件必须全部被一起编译,一个包即是编译时的一个单元,因此根据惯例,每个目录都只包含一个包。

如果对一个包进行更改或重新编译,所有引用了这个包的客户端程序都必须全部重新编译。

Go 中的包模型采用了显式依赖关系的机制来达到快速编译的目的,编译器会从后缀名为 .o 的对象文件(需要且只需要这个文件)中提取传递依赖类型的信息。

如果 A.go 依赖 B.go,而 B.go 又依赖 C.go:

  • 编译 C.go, B.go, 然后是 A.go.
  • 为了编译 A.go, 编译器读取的是 B.o 而不是 C.o.

导入

如果包名不是以 . 或 / 开头,如 "fmt" 或者 "container/list",则 Go 会在全局文件进行查找;如果包名以 ./ 开头,则 Go 会在相对目录中查找;如果包名以 / 开头(在 Windows 下也可以这样使用),则会在系统的绝对路径中查找。

导入单个包:

// 单个包
import "fmt"

多个包

// 多个
import "fmt"
import "os"
// 多个
import (
"fmt"
"os"
)

别名

// 别名 alias
import (
io "fmt"
)
func main(){
io.Println("123")
}

省略

// 省略调用,不建议使用
import . "fmt"
Println("1231132")

如果导入一个包没有是使用它,在构建程序时,会抛出错误imported and not used: os

可见性规则

Go 语言中,使用大小写来决定该常量、变量、类型、接口、结构或函数是否可以被外部包所调用:

根绝约定,函数名首字母小写即为 private,对包外不可见,在包内是可以可见并可用的

func getField(i int)

函数名首字母大写为 public,被视为导出

func Printf(i int)

func 函数

main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数,如果有 init() 函数则会先执行该函数。

// 当前程序的包名
package main
// 导入其他的包
import fm "fmt"
func init() {
fm.Println("-- func init execute --")
}
func main() {
// 输出
fm.Printf("Hello,Go\n你好,狗")
}

输出:

-- func init execute --
Hello,Go
你好,狗
Process finished with exit code 0

如果你的 main 包的源代码没有包含 main 函数,则会引发构建错误 undefined: main.main。main 函数既没有参数,也没有返回类型(与 C 家族中的其它语言恰好相反)。如果你不小心为 main 函数添加了参数或者返回类型,将会引发构建错误:

func main must have no arguments and no return values results.
或者
Run after build is not possible Main file has non-main package or doesn't contain main function

注释

几乎所有全局作用域的类型、常量、变量、函数和被导出的对象都应该有一个合理的注释。如果这种注释(称为文档注释)出现在函数前面,例如函数 Abcd,则要以 "Abcd..." 作为开头。

// enterOrbit causes Superman to fly into low Earth orbit, a position
// that presents several possibilities for planet salvation.
func enterOrbit() error {
...
}

Go 类型与变量

ZRoger ZRoger #Go

Go 的类型与变量:基本类型与零值、类型别名、单/多个变量的声明与赋值、类型推导、类型转换,以及指针。

类型

可以包含数据的变量(或常量),可以使用不同的数据类型或类型来保存数据。使用 var 声明的变量的值会自动初始化为该类型的零值。类型定义了某个变量的值的集合与可对其进行操作的集合。

基本类型

类型可以是基本类型,如:int、float、bool、string;

  • 布尔型 bool

    • 长度:1字节
    • 取值范围:[true,false]
    • 注意事项:不可以用数字代表true或false
  • 整型:int/uint

    • 根据运行平台可能为32位或64位
  • 8位整型:int8/unit8

    • 长度:1字节
    • 取值范围:-128127 / 0255
  • 16位整型:int16/unit16

    • 长度:2字节
    • 取值范围: [−216/2⟶216/2−1] / [0⟶216−1]
  • 32位整型:int32(rune别名)/unit32

    • 长度:4字节
    • 取值范围: [−232/2⟶232/2−1] / [0⟶232−1]
  • 64位整型:int64/unit64

    • 长度:8字节
    • 取值范围: [−264/2⟶264/2−1] / [0⟶264−1]
  • 浮点型:float32/float64

    • 长度:4/8字节
    • 小数点:精确到7/15位小数点
  • 字节型: byte(unit8别名)

  • 复数: complex64/complex128

    • 长度:8/16字节
  • 足够保存指针的32位或64位整数型:uintptr

  • 其他值类型:

    • array
    • struct
    • string
  • 引用类型

    • slice
    • map
    • chan
  • 接口类型

    • interface
  • 函数类型

    • func

关于取值范围

有符号的需要区分正数,负数和零。基本是2的n次方,之后除于2,在取出0。

像:8位整型有符号是 - 2^8/2 ~ 2^8/2-1 就是 -128127 / 0255

无符号则是从0开始,最后再减一。

类型零值

零值并不等于空值,而是当变量被声明为某种类型后的默认值,通常情况下值类型的默认值为0,bool为false,string为空字符串。结构化的类型没有真正的值,它使用 nil 作为默认值(在 Objective-C 中是 nil,在 Java 中是 null,在 C 和 C++ 中是NULL或 0)。值得注意的是,Go 语言中不存在类型继承。

func main() {
var a [5]int
fm.Println(a)
}
# export
[0 0 0 0 0]

类型别名

type (
byte uint8
rune int32
str string
)
var param str
param = "hello"

变量

单个变量的声明与赋值

  • 变量的声明格式: var <变量名称> <变量类型>
  • 变量的赋值格式:<变量名称> = <表达式>
  • 声明的同时赋值: var <变量名称>[变量类型] = <表达式>
var 变量名 [类型] [=] [初始值]
// 变量的声明与赋值
var a int
a = 123
// 同时赋值
var b int = 2
// 类型由系统推断
var c = 321
// 最简写法
d := 233
fm.Println(a,b,c,d)

类型推导

如果在公开的自定义类型(结构)中,组合了只对自己可见的类型,且此类型的属性是公开的。其他地方在获取这个私有的类型时,只能通过:=去获取,因为无法直接使用这个类型,所以只能通过:=进行类型自动推断。

如何用一行代码交换整数 i 和 j 的值

i,j=j,i;

多个变量的声明与赋值

  • 全局变量的声明可以使用var()方式进行简写
  • 全局变量的声明不可以省略var,但可使用并行方式
  • 所有变量都可以使用类型推断
  • 局部变量不可以使用var()的方式简写,只能使用并行方式
var (
// 使用常规方法
str1 = "hello "
// 使用并行方法以及类型推断
str2, str3 = "Go's", "World"
// str4 := 3 不可以省略 var
)
fm.Println(str1 + str2 + str3)
// 多个变量的声明
var a, b, c, d int
// 多个变量的赋值
a, b, c, d = 1, 2, 3, 4
// 多个变量声明的同时赋值
var e, f, g, h int = 5, 6, 7, 8
// 省略变量类型,由系统推断
var i, j, k, l = 9, 10, 11, 12
// 多个变量声明与赋值的最简写法
i, m, n, o := 13, 14, 15, 16
fm.Println(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o)
// 赋值时忽略了第二个数,主要应用于函数的返回值中
a, _, c, d := 1, 2, 3, 4
fm.Println(a,c, d)

简写申明多个变量,一般称这种形式为 组

// 常量的定义 - 常量组
const (
PI = 3.14
const1 = "1"
const2 = 2
const3 = "3"
)
// 全局变量的声明与赋值 - 全局变量组
// 在函数体内不支持这种简写
var (
integerNum = 1
integerNum2 = 1
)
// 一般类型声明
type (
newType int
type1 float32
type2 string
type3 byte
)

变量的类型转换

Go 是类型安全的

  • 不存在隐士转换,所有类型转换必须显式声明

  • 转换只能发生在两种互相兼容的类型之间

  • 类型转换的格式

    • <ValueA> [:] = <TypeOfValueA>(<ValueB>)

例子:

var a float32 = 1.1
b := int(a)
fmt.Println(a,b)

以下这段中,b最后的输出是A,而不是字符串65,string()表示将数据转换成文本格式,因为计算机中存储的任何东西本质上都是数字,因此此函数自然地认为我们需要将数字65表示为文本A

func main() {
var a int = 65
b := string(a)
fmt.Println(a,b)
}

那么如何将数字65转成字符串65? 需要导入新的包strconv帮助进行转换。

import (
"fmt"
"strconv"
)
func main() {
var a int = 65
b := strconv.Itoa(a)
fmt.Println(a, b)
}
  • strconv.Itoa :将数字转成字符串
  • strconv.Atoi:将字符串转成数字

指针

Go不支持指针运算,也有垃圾回收机制。

func TestHello(t *testing.T) {
a := 1;
aPtr := &a;
t.Log(a, aPtr);
t.Logf("%T %T", a, aPtr);
}

结果:

Terminal window
1 0xc00000c2c8
int *int

试一下指针运算:

func TestHello(t *testing.T) {
a := 1;
aPtr := &a;
t.Log(a, aPtr);
+ aPtr = aPtr + 1 ;
t.Logf("%T %T", a, aPtr);
}

结果:

Terminal window
invalid operation: aPtr + 1 (mismatched types *int and int)

Go 常量与运算符

ZRoger ZRoger #Go

Go 的常量与运算符:常量定义与初始化规则、iota 枚举,以及一元与二元运算符。

常量

定义

  • 常量的值在编译时就已经确定
  • 常量的定义格式与变量基本相同
  • 等号右侧必须是常量或者常量表达式
  • 常量表达式中的函数必须是内置函数
onst a int = 1
const b = 'A'
const (
text = "123"
length = len(text)
num = b * 20
)
const i, j, k = 1, "2", '3'
const (
text2, length2, num2 = "456", len(text2), k * 10
)

初始化规则与枚举

  • 在定义常量时,如果不提供初始值,则表示将使用上行的表达式
  • 使用相同的表达式不代表具有相同的值
  • iota 是常量的计数器,从 0 开始,组中每定义1个常量自动递增1
  • 通过初始化规则与 iota 可以达到枚举的效果
  • 每遇得到一个 const 关键字, iota 就会重置为 0
const (
// a 和 b 都为 “A"
a = "A"
b
c = iota
// d 的值为 3
d
)
const (
e = iota
// f 的值为 1
f
)
// 星期枚举
const (
Monday = iota
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday
)
// 输出结果
A A 2 3
0 1
0 1 2 3 4 5 6

一般来说,在文件内中的常量尽量大写

const MAX_COUNT = 1000
// 如果不想给包外使用,根据可见性规则,可以改为
const _MAX_COUNT = 1000
// 或者
const cMAX_COUNT = 1000

iota

iota是表示无类型整数序数的预先声明的标识符,它是零索引的,Untyped int.

运算符

Go 中的运算符均是从左至右结合

优先级由高到低

运算符
一元 ^、!
二元 *、/、%、<<、>>、&、&^
+、-、`
==、!=、<、<=、>、>=、
专门用于并发的 channel <-
&&、`

一元运算符

参与计算的数字只有一个

fmt.Println(^2)
fmt.Println(!true)
// 结果
-3
false

二元运算符

参与计算的数字有2个

/**
5 0101
12 1100
--------------
& 0100 -> 4
| 1101 -> 13
^ 1001 -> 9
&^ 0001 -> 1
*/
func main() {
a := 5
b := 12
fmt.Println(a & b)
fmt.Println(a | b)
fmt.Println(a ^ b)
fmt.Println(a &^ b)
}

枚举与 iota

常量的 iota 与 << 运算符实现计算机存储单位的枚举

const (
B float64 = 1 << (iota * 10)
KB
MB
GB
TB
)

第一个常量中的iota为0,之后的KB会将上面的公式带入,不同时 iota 此时进行了 + 1 操作

Go 控制语句

ZRoger ZRoger #Go

Go 的控制语句:if 的写法与初始化表达式、只有 for 一种循环的三种形式、switch,以及 break / goto / continue 与标签。

if 控制

  • 条件表达式没有括号
  • 支持一个初始化表达式(可以是并行方式)
  • 左大括号必须和条件语句或else在同一行
  • 支持单行模式
  • 初始化语句中的变量为block级别,同时隐藏外部同名变量
a := true
if a, b, c := 1, 2, 3; a+b+c > 6 {
fmt.Println(">6")
} else {
fmt.Println("<=6")
fmt.Println(a)
}
fmt.Println(a)

for 循环

在 Go 中只有 for 一个循环语句关键字,没有 while ,但是支持3种形式

  • 初始化和步进表达式可以是多个值

  • 条件语句每次循环都会被重新检查,因此不建议在条件语句中使用函数,尽量提前计算好并以变量或常量代替

  • 左大括号必须和条件语句在同一行

// 第一种
a := 0
for {
a++
if a > 3 {
break
}
}
fmt.Println(a)
// 结果为4
// 第二种
a := 0
for a <= 3{
a ++
}
fmt.Println(a)
// 结果为4
// 第三种
a := 1
for i:= 0 ; i < 3 ; i++{
a ++
}
println(a)
// 结果为4

死循环

for {
// do something
}

switch 选择

  • 可以使用任何类型或表达式作为条件语句

  • 不需要写break,一旦条件成立自动终止

  • 如希望继续执行下一个case,需要使用 fallthrough 语句

  • 支持一个初始化表达式(可以是并行方式),右侧需要跟分号

  • 左大括号必须和条件语句在同一行

例子1,不需要写break,条件成立会自动终止:

func sF1(){
a := 1
switch a {
case 0 :
fmt.Println("a=0")
case 1:
fmt.Println("a=1")
}
fmt.Println(a)
}

输出为:

a=1
1

例子2,如果希望继续执行下一个case,使用 fallthrough :

func sF2() {
a := 1
switch {
case a >= 0:
fmt.Println("a=0")
fallthrough
case a >= 1:
fmt.Println("a=1")
}
fmt.Println(a)
}

输出为:

a=0
a=1
1

例子3,支持一个初始化表达式,需要分号结尾 :

func sF3() {
a := 1
switch b := 1; {
case a+b > 2:
fmt.Println("a+b>2")
case a+b == 2:
fmt.Println("a+b=2")
default:
fmt.Println("a+b<2")
}
fmt.Println(a)
}

输出为:

a+b<=2
1

在 switch 中 b 的作用域在 switch中有效,所以外面是输出不到它的信息

跳转语句 与 标签

  • 3个语句都可以配合标签使用
  • 标签名区分大小写,若不适用会造成编译错误
  • break 与 continue 配合标签可用于多层循环的跳出
  • goto 是调整执行位置,与其他2个语句配合标签的结果并不相同

break

func breakF() {
for i := 0; i < 3; i++ {
if i > 1 {
fmt.Println(i)
break
}
}
fmt.Println("over")
}

输出:

2
over

当 i > 1 时,输出当前的值 break 结束循环,并继续执行剩下的代码

拥有2层for时,跳到最外面,使用标签,会跳到与标签同一级,然后执行for后面的代码

func breakF1() {
LABEL:
for {
for i := 0; i < 10; i++ {
if i > 2 {
fmt.Println("break to label,i: ", i)
break LABEL
} else {
fmt.Println(i)
}
}
}
fmt.Println("over")
}

输出:

0
1
2
break to label,i: 3
over

goto

如果将 break 改为 goto ,将会导致重新进入一次循环,导致死循环

if i > 2 {
fmt.Println("break to label,i: ", i)
break LABEL
goto LBAEL
}

可以将标签放在 for 之后

LABEL:
for {
for i := 0; i < 10; i++ {
if i > 2 {
fmt.Println("break to label,i: ", i)
break LABEL
goto LBAEL
} else {
fmt.Println(i)
}
}
}
LABEL:
fmt.Println("over")

输出:

0
1
2
break to label,i: 3
over

continue

例子:

func continueF1() {
LABEL:
for i := 0; i < 3; i++ {
fmt.Println("before i", i)
for {
continue LABEL
fmt.Println(i)
}
fmt.Println("after i", i)
}
fmt.Println("over")
}

结果:

before i 0
before i 1
before i 2
over

在 continue 之后都不会执行

func continueF2() {
LABEL:
for i := 0; i < 10; i++ {
for {
fmt.Print(i)
continue LABEL
}
}
}

输出:

0123456789

接下将换成 goto 发现输出都是0,其变成了死循环。goto 是调整执行位置

000000000000000000000000000000000000000000000000000000000000000000000..............

将 label 放在 goto 的后面

func continueF2() {
for i := 0; i < 10; i++ {
for {
fmt.Print(i)
continue LABEL
}
}
LABEL:
// xxxxx
}

Go 数组

ZRoger ZRoger #Go

Go 数组:值类型语义、长度是类型的一部分、指向数组的指针与指针数组、数组比较、new 与多维数组,附冒泡排序示例。

数组 Array

  • 定义数组的格式:var <varName> [n]<type,n> = 0
  • 数组长度也是类型的一部分,因此具有不同长度的数组为不同类型
  • 注意区分指向数组的指针和指针数组
  • 数组在Go中为值类型
  • 数组之间可以使用==或者!=进行比较,但是不可以使用>或者<
  • 可以使用new来创建数组,此方法返回一个指向数组的指针
  • Go支持多维数组

例子

func arrF1() {
var a [2]int
fmt.Println(a)
// 简写
a1 := [2]int{}
fmt.Println(a1)
a2 := [2]int{1}
fmt.Println(a2)
a3 := [2]int{1, 2}
fmt.Println(a3)
}

输出

[0 0]
[0 0]
[1 0]
[1 2]

如果字面值不够,会用0值来补充剩余的元素

索引赋值

前4个数字为0,最后一个数字为1

a4 := [5]int{4: 1}
fmt.Println(a4)

输出

[0 0 0 0 1]

在知道具体元素的情况下,也可以不声明数组长度

a5 := [...]int{1, 2, 3, 4, 5}
fmt.Println(a5)

输出

[1 2 3 4 5]

或者根据索引进行赋值,输出同上

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

前一位为索引,后一位为值

对于a4 := [5]int{4: 1}也可以使用另外一种写法,不指定数组长度

a7 := [...]int{4: 1}
fmt.Println(a7)

输出

[0 0 0 0 1]

指向数组的指针和指针数组

指向数组的指针

a7 := [...]int{4: 1}
var p *[5]int = &a7
fmt.Println(p)

输出

&[0 0 0 0 1]

即 p 指针是数组[0 0 0 0 1]的地址

指针数组

x, y := 1, 2
a8 := [...]*int{&x, &y}
fmt.Println(a8)

输出:

[0xc00001a178 0xc00001a180]

数组在Go中为值类型

在其他语言中,数组为引用类型,在Go中为值类型,当函数中数组参数时

func test(arr1){
// 是将数组的值进行了copy,而不是传递地址给函数
}

数组比较

数组类型要相同,长度一样

a9 := [...]int{1, 2}
a10 := [...]int{3, 4}
fmt.Println(a9 == a10)

输出

false

new 关键字

p2 := new([10]int)
fmt.Println(p2)

输出

&[0 0 0 0 0 0 0 0 0 0]

指向数组的指针。

都可以使用下标的方式去修改数组

a := [10]int{}
a[1] = 2
fmt.Println(a)
p2 := new([10]int)
p2[1] = 2
fmt.Println(p2)

输出

[0 2 0 0 0 0 0 0 0 0]
&[0 2 0 0 0 0 0 0 0 0]

多维数组

func arrF2() {
a := [2][3]int{
{1, 1, 1},
{2, 2, 2},
}
fmt.Println(a)
}

输出

[[1 1 1] [2 2 2]]

索引赋值

func arrF2() {
a := [2][3]int{
{1:3},
{0:4},
}
fmt.Println(a)
}

输出

[[0 3 0] [4 0 0]]

但是与一维不同的是,不能用[...]

冒泡排序

func popF(){
a := [...]int{5,2,6,3,9}
fmt.Println(a)
num := len(a)
for i:= 0 ; i<num ; i++{
for j := i + 1 ; j <num ; j++ {
if a[i] < a[j] {
a[i],a[j] = a[j],a[i]
}
}
}
fmt.Println(a)
}

Go 切片

ZRoger ZRoger #Go

Go 切片:与底层数组的关系、len 与 cap、make、reslice、append 与 copy,以及多个切片共享底层数组时的相互影响。

切片Slice

切片本身并不是数组,是指向底层的数组,类似指针。其作为变长数组的替代方案,可以关联底层数组的局部或全部。同时这是一个引用类型,可以直接传教或者从底层数组获取生成。和数组一样,可以使用len()来获取元素个数,cap()获取容量。一般使用mark()进行创建,而不是new ()。

如果多个slice指向相同的底层数组,其中一个的值改变会影响到全部

make([]T, len, cap)

其中cap是可以省略的,默认为与len()值相同

简单例子

var a []int
fmt.Println(a)

输出

[]

取元素

取前/后n位元素

func sliceF2() {
a := [10]int{1, 2, 4, 5, 6, 7, 8, 9, 10}
fmt.Println(a)
s1 := a[5:10]
fmt.Println(s1)
}

输出

[1 2 4 5 6 7 8 9 10 0]
[7 8 9 10 0]

包含起始索引,不包含结尾的索引

可以简化下

s1 := a[5:10]
s1 := a[5:]
// 或者
s1 := a[5:len(a)]

往前面取,索引从0~4

s1 := a[:5]

make

func sliceF3() {
// 类型,元素个数,容量
s1 := make([]int, 3, 10)
fmt.Println(len(s1), cap(s1))
}

当元素个数小于等于容量时,不会进行扩容。当大于当前容量时,会分配长度位20且连续的内存块。每次超过时,容量值会翻倍,10->20->40。

容量默认为元素格式

Reslice

Reslice 时索引以被slice的切片为准,索引不可以超过被slice的切片容量cap()值,索引越界不会导致底层数组的重新分配而是引发错误

func sliceF4() {
a := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'}
sa := a[2:5]
// 在sa的范围内
sb := sa[0:2]
// 超出sa的范围,但后面的内容是连续的
sc := sa[3:5]
fmt.Println(string(a))
fmt.Println(string(sa), len(sa), cap(sa))
fmt.Println(string(sb))
fmt.Println(string(sc))
}

输出

abcdefghijk
cde 3 9
cd
fg

sa 从'c'的位置开始,一直到a结束,是一个连续的,所分配的最大容器是 9

Append

在slice尾部追加元素,可以将一个slice追加到另一个slice的尾部,如果最终长度未超过追加到slice的容量则返回原始slice。如果超过,则将会重新分配数组并拷贝原始数据

func sliceF5() {
s1 := make([]int, 3, 6)
fmt.Printf("%p\n", s1)
s1 = append(s1, 1, 2, 3)
fmt.Printf("%v %p\n", s1, s1)
}

输出

0xc0000d6030
[0 0 0 1 2 3] 0xc0000d6030

超出时,重新分配并拷贝原始数据

func sliceF5() {
s1 := make([]int, 3, 6)
fmt.Printf("%p\n", s1)
s1 = append(s1, 1, 2, 3)
fmt.Printf("%v %p\n", s1, s1)
s1 = append(s1, 1, 2, 3)
fmt.Printf("%v %p\n", s1, s1)
}

输出

0xc0000d6030
[0 0 0 1 2 3] 0xc0000d6030
[0 0 0 1 2 3 1 2 3] 0xc000086060

同时指向的影响

多个slice指向同一个底层数组,在操作数组时,会影响到所有关联的slice

func sliceF6() {
a := []int{1, 2, 3, 4, 5, 6}
s1 := a[0:4]
s2 := a[0:3]
fmt.Println(s1, s2)
s1[1] = 123go
fmt.Println(s1, s2)
}

输出

[1 2 3 4] [1 2 3]
[1 123 3 4] [1 123 3]

但对slice2进行append操作,并超过其cap()值时,之后的操作就不会影响到slice2

func sliceF6(){
a := []int{1, 2, 3, 4, 5, 6}
s1 := a[0:4]
s2 := a[0:3]
fmt.Println(s1, s2)
s2 = append(s2, 1, 1, 1, 1, 1, 1, 1, 1)
a[1] = 123
fmt.Println(s1, s2)
}

输出

[1 2 3 4] [1 2 3]
[1 123 3 4] [1 2 3 1 1 1 1 1 1 1 1]

Copy

func sliceF8() {
s1 := []int{1, 2, 3, 4, 5, 6}
s2 := []int{7, 8, 9}
copy(s2, s1)
fmt.Println(s2)
}

输出

[1 2 3]

s2 中只有3个元素,将会copy前3个元素

只copy s1中的一部分,s1[0:2]是一个新的slice

copy(s2, s1[0:2])
copy(s2, s1)

输出

[1 2 9]

或者只是替换其中一部分

copy(s2[1:2], s1[0:2])
copy(s2, s1)

输出

[7 1 9]

得到一个完整的数组

s1 := []int{1, 2, 3, 4, 5, 6}
s3 := s1
s4 := s1[0:]
s5 := s1[0:len(s1)]
s6 := s1[:len(s1)]
s7 := s1[:]
fmt.Println(s3)
fmt.Println(s4)
fmt.Println(s5)
fmt.Println(s6)
fmt.Println(s7)

输出

[1 2 3 4 5 6]
[1 2 3 4 5 6]
[1 2 3 4 5 6]
[1 2 3 4 5 6]
[1 2 3 4 5 6]

Go map

ZRoger ZRoger #Go

Go 的 map:make 创建与初始化、增删改查、容量与扩容,以及以切片或 map 为值的复杂用法与排序。

map

类似其他语言中的哈希表或者字典,以key-value形式存储数据,key必须是支持==或者!-比较运算的类型,不可以是函数,map或者slice。

Map 查找比线性搜索快很多,但比使用索引访问数据的类型慢100倍。

Map使用make()创建,支持:=简写方式

make([keyType]valueType, cap),其中cap表示容量,可省略

超出容量时会自动扩容,但尽量提供一个合理的初始值

键值对不存在时会自动添加,使用delete()删除某键值对

使用for range对map和slice进行迭代操作

初始化

func mapF1() {
var m1 map[int]string
m1 = map[int]string{}
fmt.Println(m1)
var m2 map[int]string
m2 = make(map[int]string)
// var m2 map[int]string = make(map[int]string)
fmt.Println(m2)
m3 := map[int]string{}
fmt.Println(m3)
m4 := make(map[int]string)
fmt.Println(m4)
}

输出

map[]
map[]
map[]
map[]

元素操作

添加

func mapF2(){
m := map[string]string{}
m["name"] = "Bob"
m["age"] = "18"
fmt.Println(m)
a := m["name"]
fmt.Println(a)
}

输出

map[age:18 name:Bob]
Bob

删除

delete(m,"name")
fmt.Println(m)

输出

map[age:18]

复杂一点的map

func mapF3() {
// 1 : {1:"123"}
m := make(map[int]map[int]string)
m2 := make(map[int]string)
m2[0] = "Bob"
m[0] = m2
fmt.Println(m2)
fmt.Println(m)
}

输出

map[0:Bob]
map[0:map[0:Bob]]

单独初始化

func mapF3() {
// 1 : {1:"123"}
m := make(map[int]map[int]string)
a, ok := m[0][0]
fmt.Println(a, ok)
if !ok {
m[0] = make(map[int]string)
}
m[0][0] = "OK"
fmt.Println(m)
}

输出

false
map[0:map[0:OK]]

循环迭代

slice

func sliceDemo() {
a := []int{1, 2, 3, 4, 5, 6}
for i, v := range a {
fmt.Println(i, v)
}
}

输出

0 1
1 2
2 3
3 4
4 5
5 6

i是索引,v是具体的属性值,v是个拷贝,对其操作不会影响原来的map

m := make([]map[int]string, 5)
for _, v := range m {
v = make(map[int]string, 1)
v[1] = "OK"
fmt.Println(v)
}
fmt.Println(m)

输出

map[1:OK]
map[1:OK]
map[1:OK]
map[1:OK]
map[1:OK]
[map[] map[] map[] map[] map[]]

m中并没有"OK"。所以为了影响map中,需要使用到索引,即第一个参数

func mapF6() {
m := make([]map[int]string, 5)
for i := range m {
m[i] = make(map[int]string, 1)
m[i][1] = "OK"
fmt.Println(m[i])
}
fmt.Println(m)
}

_为省略

输出

map[1:OK]
map[1:OK]
map[1:OK]
map[1:OK]
map[1:OK]
[map[1:OK] map[1:OK] map[1:OK] map[1:OK] map[1:OK]]

map

m := make(map[int]string)
m[0] = "Hello"
m[1] = "Jmy"
m[2] = "Nice to meet you"
for i, v := range m {
fmt.Println(i, v)
}

输出

0 Hello
1 Jmy
2 Nice to meet you

对map中元素进行操作

for i, v := range m {
m[i] = v + " 123"
}

输出

map[0:Hello 123 1:Jmy 123 2:Nice to meet you 123]

排序

无序性
func mapF7() {
m := map[int]string{1: "a", 2: "b", 3: "c", 4: "d", 5: "e"}
s := make([]int,len(m))
i := 0
for key,_ := range m{
s[i] = key
i ++
}
fmt.Println(s)
}

输出

[5 1 2 3 4]
sort 包
import (
"fmt"
"sort"
)
.... 省略
sort.Ints(s)
fmt.Println(s)

输出

[1 2 3 4 5]

练习

func mapF8() {
m1 := map[int]string{1: "a", 2: "b", 3: "c"}
m2 := make(map[string]int,len(m1))
for key,value := range m1{
m2[value] = key
}
fmt.Println(m1)
fmt.Println(m2)
}

输出

map[1:a 2:b 3:c]
map[a:1 b:2 c:3]

Go 函数

ZRoger ZRoger #Go

Go 函数:不定长变参、多返回值与命名返回值、函数类型、匿名函数与闭包、defer,以及 panic 与 recover。

函数

Go 函数不支持嵌套,重载和默认参数

但支持以下特性:

  • 无需声明原型,不定长度变参,多返回值,命名返回值参数,匿名函数,闭包

定义函数使用关键字func,且左大括号不能另起一行

函数也可以作为一种类型使用

无需声明原型

func funcTest1(a int, b string) (int, string, int) {
fmt.Println(a, b)
return 1, "123", 123
}
func main() {
fmt.Println(funcTest1(1, "234"))
}

多参数同类型

参数

func funcTest2(a, b, c int) {
fmt.Println(a, b, c)
}

返回值

func funcTest2(a, b, c int) (a1, b1, c1 int) {
fmt.Println(a, b, c)
a1, b1, c1 = a+1, b+1, c+1
return
}

return 与返回声明中的参数名一样就可以省略不用写,不过一般情况下,按照编码要求还是需要写得

func funcTest2(a, b, c int) (a1, b1, c1 int) {
fmt.Println(a, b, c)
a1, b1, c1 = a+1, b+1, c+1
return a1, b1, c1
}

不定长度变参

java 中参数可变的例子

private static int sumUp(int... values) {
}

在go中也可以

func funcTest3(a ...int) int {
max := math.MinInt64
for i, _ := range a {
fmt.Println(a[i])
if a[i] > max {
max = a[i]
}
}
return max
}

...[type]必须是最后一个参数,是一个slice

以下是错误的

func funcTest(a ...int, b string)

正确

func funTest(b string, a ...int)

...[type]可变参数可以看作是一个slice,但是与slice不同的是,不是引用类型,实际上得到是一个值拷贝。

如果参数是slice,是拷贝slice的内存地址,而不是值。

func funcTest5(s []int) {
for i,v := range s {
s[i] = v + 1
}
fmt.Println("func:", s)
}
func main() {
s := []int{1, 2, 3}
fmt.Println("before:", s)
funcTest5(s)
fmt.Println("after", s)
}

输出

before: [1 2 3]
func: [2 3 4]
after [2 3 4]

一般类型都是值拷贝,如果需要影响到传入的值,需要传入值的地址。

func funcTest5(s []int) {
func funcTest5(s []int, a *int) {
for i, v := range s {
s[i] = v + 1
}
*a = 2
fmt.Println("func:", s)
fmt.Println("func:", s, a)
}
func main() {
s := []int{1, 2, 3}
fmt.Println("before:", s)
funcTest5(s)
fmt.Println("after", s)
a := 1
fmt.Println("before:", s, a)
funcTest5(s, &a)
fmt.Println("after", s, a)
}

输出

before: [1 2 3] 1
func: [2 3 4] 0xc00001a098
after [2 3 4] 2

函数也可以作为一种类型使用

这个特性与JavaScript中差不多,定义一个变量等于函数,使用这个变量相当于使用这个函数。在Go中一切皆类型

func funcTest6() {
fmt.Println("hello everyone")
}
func main() {
a := funcTest6
a()
}

输出

hello everyone

匿名函数

func main() {
a := func() {
fmt.Println("这是一个匿名函数")
}
a()
}

没有命令函数名称。是一个代码块。

闭包

func funcTest7(x int) func(int) int {
fmt.Println("func test 7", x, &x)
return func(y int) int {
x++
fmt.Println("func test 7 ", x, &x, y)
return x + y
}
}
func main() {
f := funcTest7(10)
fmt.Println(f(1))
fmt.Println(f(2))
}

输出

func test 7 10 0xc00001a098
func test 7 11 0xc00001a098 1
12
func test 7 12 0xc00001a098 2
14

3次输出x的地址是一样的。

defer

执行方式类似其他语言中的析构函数,在函数体执行结束后按照调用顺序的相反顺序逐个执行(先进后出,栈堆)。即使函数发生严重错误也会执行,支持匿名函数的调用。通常用于资源清理,文件关闭,解锁以及记录时间等操作。通过与匿名函数配合可在return之后修改函数计算结果。如果函数体内某个变量作为defer时匿名函数的参数,则在定义defer时即已经获得了拷贝,否则引用某个变量的地址。。

Go 中没有异常机制,但是有 panic/recover模式来处理错误

panic可以在任何地方引发,但是recover只有在defer调用的函数中有效

相反顺序

func main() {
fmt.Println("a")
defer fmt.Println("b")
defer fmt.Println("c")
}

输出

a
c
b

按照调用顺序的相反顺序逐个执行

func main() {
for i := 0; i < 3; i++ {
defer fmt.Println(i)
}
}

输出

2
1
0

闭包

在这个匿名函数中,i被当作一个地址的引用,引用这个局部变量,当循环结束时,i = 3 。在main函数结束后,开始执行defer中的代码。

for i := 0; i < 3; i++ {
defer func() {
fmt.Println(i)
}()
}

输出

3
3
3

可以改成这样:

for i := 0; i < 3; i++ {
defer func(a int) {
fmt.Println(a)
}(i)
}

输出

2
1
0

panic

func funcTest8(){
fmt.Println("func test 8")
}
func funcTest9(){
panic("func test 9")
}
func funcTest10(){
fmt.Println("func test 10")
}
func main() {
funcTest8()
funcTest9()
funcTest10()
}

输出

func test 8
panic: func test 9
goroutine 1 [running]:
main.funcTest9(...)
... 省略

并没有继续往下执行到test10

recover()

func funcTest9() {
defer func() {
if err := recover(); err != nil {
fmt.Println("Recover in test 9")
}
}()
panic("func test 9")
}

需要在panic前面加入以上代码,输出

func test 8
Recover in test 9
func test 10

练习

func main() {
var fs = [4]func(){}
for i := 0; i < 4; i++ {
defer fmt.Println("defer i = ", i)
defer func() {
fmt.Println("defer_closure i = ", i)
}()
fs[i] = func() {
fmt.Println("closure i = ", i)
}
}
for _, f := range fs {
f()
}
}

输出

closure i = 4
closure i = 4
closure i = 4
closure i = 4
defer_closure i = 4
defer i = 3
defer_closure i = 4
defer i = 2
defer_closure i = 4
defer i = 1
defer_closure i = 4
defer i = 0

fs[i]() 中的i,因为使用的是外部的i,实际上调用外部 i的地址,当循环体结束时,i = 4 ,所以输出closure i = 4

defer 使用的匿名函数与上面一样,是闭包的思想。

Go 结构体

ZRoger ZRoger #Go

Go 的结构体:替代 class 的定义方式、值拷贝与指向结构的指针、匿名结构与匿名字段、嵌入 struct,以及类型比较。

结构

Go 中的struct与 C 中的 struct非常相似,并且 Go 没有 class 。使用 type<Name> struct{}定义结构,名称遵循可见性规则。

  • 支持指向自身的指针类型成员
  • 支持匿名结构,可用作成员或定义成员变量
  • 匿名结构也可以用于map的值
  • 可以使用字面值对结构进行初始化
  • 运行直接通过指针来读写结构成员
  • 相同类型的成员可进行直接拷贝赋值
  • 支持==与!=比较运算符,但是不支持>和<
  • 支持匿名字段,本质上是定于了以某个类型名为名称的字段
  • 嵌入结构作为匿名字段看起来像继承,但不是继承
  • 可使用匿名字段指针
type person struct {
name string
age int
mobile int
}
func main() {
amy := person{"Amy", 18, 112340259}
fmt.Println(amy)
}

输出

{Amy 18 112340259}

操作

类似像JavaScript中对象属性的操作

func main() {
amy := person{"Amy", 18, 112340259}
amy.age = 22
fmt.Println(amy)
}

输出

{Amy 22 112340259}

值拷贝

作为函数的参数时,是一个值拷贝,而不是引用类型

func structTest1(a person) {
a.age = 30
fmt.Println("struct test 1", a)
}
func main() {
amy := person{"Amy", 18, 112340259}
amy.age = 22
fmt.Println(amy)
structTest1(amy)
fmt.Println("after:", amy)
}

输出

{Amy 22 112340259}
struct test 1 {Amy 30 112340259}
after: {Amy 22 112340259}

可以发现age在函数执行后并没有发生改变。如果需要改变,需要传递指针(地址)

func structTest1(a *person) {
func structTest1(a person) {
a.age = 30
fmt.Println("struct test 1", a)
}
func main() {
amy := person{"Amy", 18, 112340259}
amy.age = 22
fmt.Println(amy)
structTest1(amy)
structTest1(&amy)
fmt.Println("after:", amy)
}

输出

{Amy 22 112340259}
struct test 1 &{Amy 30 112340259}
after: {Amy 30 112340259}

指向结构的指针

在初始化时,变成结构的指针,也可以达到上面的效果。

amy := person{"Amy", 18, 112340259}
amy := &person{"Amy", 18, 112340259}
... 省略代码
structTest1(amy)
structTest1(&amy)

在对一个结构进行初始化时,习惯性使用取地址符号&,变量就变成指向某个结构的指针,方便在下面编码过程中特地去获取地址。且也可以直接对结构中的属性直接进行操作

匿名结构

没定义结构的名称,临时声明了一个结构,并对字面值进行了初始化

dog := &struct {
color string
tail string
}{
"white",
"long",
}
fmt.Println(dog)

输出

&{white long}

结构体中

type person struct {
name string
age int
mobile struct {
phone, address string
}
}
func main() {
a := &person{name: "amy", age: 18}
a.mobile.phone = "123"
a.mobile.address = "china"
fmt.Println(a)
}

输出

&{amy 18 {123 china}}

匿名字段

在结构体中未声明属性名

type pet struct {
string
int
}
func main() {
a := &pet{"dog", 2}
fmt.Println(a)
}

输出

&{dog 2}

如果此时交换初始化参数的位置,会发生错误

a := &pet{"dog", 2}
a := &pet{2, "dog"}

输出

# command-line-arguments
.....: cannot use 2 (type untyped int) as type string in field value
.....: cannot use "dog" (type untyped string) as type int in field value

类型比较操作

同类型之间的可以进行比较,作==和!=,不能作>或者<

a := pet{name: "Amy", age: 18}
b := pet{name: "Amy", age: 18}
fmt.Println(a == b)

输出

true

如果内容一致,但是类型不一样

a := pet{name: "Amy", age: 18}
b := dog{name: "Amy", age: 18}
fmt.Println(a == b)

输出

# command-line-arguments
.....: invalid operation: a == b (mismatched types pet and dog)

嵌入struct

type human struct {
name string
age int
sex int
}
type teacher struct {
human
class string
}
type student struct {
human
classRoom string
}
func main() {
a := teacher{human: human{"A", 12, 1}, class: "english"}
b := student{human{"a", 2, 0}, "2002"}
b.name = "aa"
fmt.Println(a, b)
}

输出

{{A 12 1} english} {{aa 2 0} 2002}

练习

如果匿名字段和外层结构有同名字段,应该如何进行操作

type basePo struct {
id string
}
type bizBasePo struct {
basePo
id string
}
func main() {
bill := bizBasePo{id: "1", basePo: basePo{id: "2"}}
fmt.Println(bill.id, bill.basePo.id)
}

输出

1 2

bizBasePo与basePo存在同名字段

bill := bizBasePo{id: "1", basePo: basePo{id: "2"}}
bill.name = "3"
fmt.Println(bill.id, bill.basePo.id)

输出

3 2

如果直接改成这样

type basePo struct {
id string
}
type bizBasePo struct {
basePo
id string
}

输出

2 2

如果是多个同名

type basePo struct {
id string
}
type flowPo struct {
id string
}
type bizBasePo struct {
basePo
flowPo
}
func main() {
bill := bizBasePo{basePo: basePo{id: "2"}, flowPo: flowPo{id: "5"}}
fmt.Println(bill.id, bill.basePo.id)
}

输出

# command-line-arguments
...: ambiguous selector bill.id

编译错误,此时使用那个嵌入体中的字段

可以改成这样

type basePo struct {
id string
}
type flowPo struct {
basePo
id string
}
type bizBasePo struct {
flowPo
}
func main() {
bill := bizBasePo{flowPo: flowPo{id: "5", basePo: basePo{id: "2"}}}
fmt.Println(bill.id, bill.flowPo.id, bill.flowPo.basePo.id)
}

Go 方法

ZRoger ZRoger #Go

Go 的方法:用 receiver 与类型组合、不存在重载、值接收者与指针接收者、Method Value 与 Method Expression,以及方法访问权限。

方法 Method

Go 中虽然没有class,但依旧有method,通过显示说明receiver来实现与某个类型的组合,只能为同一个包中的类型定义方法,receiver可以是类型的值或指针。

method不存在方法重载,可以是使用值或指针来调节方法,编译器会自动完成转换,从某种意义上来说,方法是函数的语法糖,因为receiver其实就是方法所接收的第一个参数(Method Value vs Method Expression)

如果外部结构和嵌入结构存在同名方法,则优先调用外部结构的方法,类型别名不会拥有底层类型所附带的方法。

方法可以待用结构中的非公开字段

例子

type SysUser struct {
name string
}
func (user SysUser) ToString() {
user.name = "123"
fmt.Println(user.name)
}
func main() {
user := SysUser{name: "1"}
user.ToString()
}

输出

123

其中ToString接收体是SysUser

不存在重载,通过类型绑定区分

type SysUser struct {
name string
}
type SysOrg struct {
name string
}
func (user SysUser) ToString() {
user.name = "123"
fmt.Println(user.name)
}
func (org SysOrg) ToString() {
org.name = "321"
fmt.Println(org.name)
}
func main() {
user := SysUser{name: "1"}
user.ToString()
org := SysOrg{name: "2"}
org.ToString()
}

输出

123
321

在这里,ToString分别绑定了SysUser和SysOrg,区分开了2种。

以下这种是不行的,以下函数针对同一个类型,区分不出来用户使用那一个。

func (user SysUser) ToString() {
user.name = "123"
fmt.Println(user.name)
}
func (user SysUser) ToString(i int) {
user.name = i
fmt.Println(user.name)
}

编译错误:

# command-line-arguments
: method redeclared: SysUser.ToString
method(SysUser) func()
method(SysUser) func(int)
: SysUser.ToString redeclared in this block
previous declaration at .....
.....: cannot use i (type int) as type string in assignment

可以是值或指针

如果是值类型,只是拷贝值,进行值传递。如果是指针类型,最终会影响到地址对应的值

func (user *SysUser) ToString() {
user.name = "123"
fmt.Println(user.name)
}
func (org SysOrg) ToString() {
org.name = "321"
fmt.Println(org.name)
}
func main() {
user := SysUser{name: "1"}
user.ToString()
fmt.Println(user)
org := SysOrg{name: "2"}
org.ToString()
}

输出

123
{123}
321

可以看到值发生了改变

底层类型

type TZ int
func (a *TZ) Print() {
fmt.Println("TZ")
}
func main() {
var a TZ
a.Print()
}

输出

TZ

底层类型就可以自由加入方法的绑定

对比

Method Value

通过值进行调用

var a TZ
a.Print()

Method Expression

通过一种类型进行调用

(*TZ).Print(&a)

方法访问权限与字段

type A struct{
name string
Age int
}

在同一个包内就可以进行访问,修改。当超出包的范围后,只有首字母大写字段暴露出去。

练习

声明一个底层类型为int的类型,并实现调用方法递增到100

func (a *TZ) Increase(num int) {
*a += TZ(num)
}
func main() {
var a TZ
a.Increase(100)
fmt.Print(a)
}

输出

100

Go 接口

ZRoger ZRoger #Go

Go 的接口:Structural Typing、接口的嵌入、类型断言、空接口与 type switch,以及接口转换与使用注意事项。

接口 interface

接口是一个或多个方法签名的集合,只要某个类型拥有该接口的所有方法签名,即算实现该接口,无需显示声明了实现了哪个接口,这称为 Structural Typing

接口只有方法声明,没有实现,没有数据类型

接口可以匿名嵌入其他接口,或嵌入结构中

将对象赋值给接口时,会发生拷贝,而接口内部存储的是指向这个复制品的指针,即无法修改复制品的状态 ,也无法获取指针

只有当接口存储的类型和对象都为 nil 时,接口才等于 nil

接口调用不会做 receiver 的自动转换

接口同样支持匿名字段方法

接口可实现类型 OOP 中的多态

空接口可以作为任何类型数据的容器

接口定义与基本操作

接口是一个或多个方法签名的集合,只要某个类型拥有该接口的所有方法签名,即算实现该接口,无需显示声明了实现了哪个接口,这称为 Structural Typing

type USB interface {
Name() string
Connect()
}
type PhoneConnector struct {
name string
}
func (pc PhoneConnector) Name() string {
return pc.name
}
func (pc PhoneConnector) Connect() {
fmt.Println("Connect:", pc.name)
}
func main() {
var a USB
a = PhoneConnector{"huawei"}
a.Connect()
}

输出

Connect: huawei

PhoneConnector拥有USB接口所有的方法,即实现了该接口

符合该interface的方法

func Disconnect(usb USB) {
fmt.Println("Disconnected.")
}
func main(){
... 省略
Disconnect(a)
}

输出

Disconnected.

嵌入接口

一个接口中包含另一个接口

type USB interface {
Name() string
Connector
}
type Connector interface {
Connect()
}

类型断言

interface.(struct)

func Disconnect(usb USB) {
if pc, ok := usb.(PhoneConnector); ok {
fmt.Println("DisConnected.", pc.name)
return
}
fmt.Println("Unknown dec.")
}

空接口与 type switch

// 空接口
type empty interface {}

意味着在Go中所有的类型都实现空接口

func Disconnect(usb USB) {
func Disconnect(usb interface{}) {

空接口以为所有的类型都是可以进入,如果要判断某一类型时,使用type switch进行处理:

func Disconnect(usb interface{}) {
switch v := usb.(type) {
case PhoneConnector:
fmt.Println("Disconnected", v.name)
default:
fmt.Println("Unknown dec.")
}
}

接口转换

由基础往下进行转换

pc := PhoneConnector{"phone"}
var a Connector
a = Connector(pc)
a.Connect()

接口使用注意事项

将对象赋值给接口时,会发生拷贝,而接口内部存储的是指向这个复制品的指针,即无法修改复制品的状态 ,也无法获取指针

func main() {
pc := PhoneConnector{"phone"}
var a Connector
a = Connector(pc)
a.Connect()
pc.name = "pc"
a.Connect()
}

输出

Connect: phone
Connect: phone

只有值拷贝

Go 反射

ZRoger ZRoger #Go

Go 的反射:用 TypeOf / ValueOf 从接口获取目标对象信息、匿名字段处理、可修改状态所需的 settable 条件,以及动态调用方法。

反射

反射可以提高程序的灵活性,使得 interface() 有更大的发挥余地,反射使用 TypeOf 和 ValueOf 函数从接口中获取目标对象信息,将匿名字段作为独立字段。

修改对象状态的前提是 interface.data 是 settable , 即 pointer-interface

通过反射可以动态调用方法

例子

type User struct {
Id int
Name string
Age int
}
func (u User) Hello() {
fmt.Println("Hello,", u.Name)
}
func Info(o interface{}) {
t := reflect.TypeOf(o)
fmt.Println("Type:", t.Name())
v := reflect.ValueOf(o)
fmt.Println("Fields")
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
val := v.Field(i).Interface()
fmt.Printf("%6s: %v = %v", f.Name, f.Type, val)
}
for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i)
fmt.Printf("%6s: %v\n", m.Name, m.Type)
}
}
func main() {
u := User{1, "OK", 12}
Info(u)
}

输出

Type: User
Fields
Id: int = 1 Name: string = OK Age: int = 12 Hello: func(main.User)

如果传递是地址

Info(u)
Info(&u)

输出

panic: reflect: NumField of non-struct type *main.User
goroutine 1 [running]:
reflect.(*rtype).NumField(0x10bcfa0, 0xc0000d2008)

加入判断

func Info(o interface{}) {
t := reflect.TypeOf(o)
fmt.Println("Type:", t.Name())
if k := t.Kind(); k != reflect.Struct{
fmt.Printf("传递参数不匹配")
return
}
.....
}

匿名

type User struct {
Id int
Name string
Age int
}
type Manager struct {
User
title string
}
func main() {
m := Manager{User{1, "OK", 12}, "123"}
t := reflect.TypeOf(m)
fmt.Printf("%#v\n", t.Field(1))
}

取匿名属性中的字段

func main() {
m := Manager{User{1, "OK", 12}, "123"}
t := reflect.TypeOf(m)
fmt.Printf("%#v\n", t.FieldByIndex([]int{0, 0}))
}

settable

x := 123
v := reflect.ValueOf(&x)
v.Elem().SetInt(999)
fmt.Println(x)

通过反射修改属性值

func Set(o interface{}) {
v := reflect.ValueOf(o)
if v.Kind() == reflect.Ptr && !v.Elem().CanSet() {
fmt.Println("error")
return
}
v = v.Elem()
if f := v.FieldByName("Name"); f.Kind() == reflect.String {
f.SetString("New Name")
}
}
func main() {
u := User{1, "OK", 12}
fmt.Println(u)
Set(&u)
fmt.Println(u)
}

输出

{1 OK 12}
{1 New Name 12}

判断是否有该属性

if !f.IsValid() {
fmt.Println("not fund")
return
}

反射调用方法

func (u User) HelloSomeone(name string) {
fmt.Println("Hello", name, ", my name is ", u.Name)
}
func main() {
u := User{1, "OK", 12}
u.HelloSomeone("Jack")
v := reflect.ValueOf(u)
mv := v.MethodByName("HelloSomeone")
args := []reflect.Value{reflect.ValueOf("Bob")}
mv.Call(args)
}

Go 并发

ZRoger ZRoger #Go

Go 的并发:goroutine、channel 的创建与关闭、缓存通道、for range 迭代、多核运行、sync.WaitGroup,以及 select。

并发concurrency

gorountine

切换时间来实现“同时”运行,在并行则是利用多核实现多线程的运行,通过通信共享内存

func Go(){
fmt.Println("Go go!")
}
func main() {
go Go()
time.Sleep(2 * time.Second)
}

Channel

channel 是 goroutine 沟通的桥梁,大都是阻塞同步的,通过 make 创建,close 关闭,channel 是引用类型,使用 for range 来迭代不断操作 channel ,可设置单向或双向通道,缓存大小,在未被填满之前不会发生阻塞

func main() {
c := make(chan bool)
go func(){
fmt.Println("123")
c <- true
}()
<- c
}

for range

func main() {
c := make(chan bool)
go func() {
fmt.Println("123")
c <- true
close(c)
}()
for v := range c {
fmt.Println(v)
}
}

缓存

c := make(chan bool, 1)

有缓存时同步阻塞的,无缓是异步的

多核运行

func Go(c chan bool, index int) {
a := 1
for i := 0; i < 10000; i++ {
a += i
}
fmt.Println(index, a)
c <- true
}
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
c := make(chan bool)
for i := 0; i < 10; i++ {
go Go(c, i)
}
for i := 0 ; i < 10 ; i++ {
<- c
}
}

同步 sync.WaitGroup

func Go(wg *sync.WaitGroup, index int) {
a := 1
for i := 0; i < 10000; i++ {
a += i
}
fmt.Println(index, a)
wg.Done()
}
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
wg := sync.WaitGroup{}
wg.Add(10)
for i := 0; i < 10; i++ {
go Go(&wg, i)
}
wg.Wait()
}

Select

可处理一个或多个 channel 的发生与接收,同时有多个可用的 channel 时按随机顺序处理,可用空的select来阻塞 main 函数,可设置超时

func main() {
c1, c2 := make(chan int), make(chan string)
o := make(chan bool)
go func() {
for {
select {
case v, ok := <-c1:
if !ok {
o <- true
break
}
fmt.Println("c1", v)
case v, ok := <-c2:
if !ok {
o <- true
break
}
fmt.Println("c2", v)
}
}
}()
c1 <- 1
c2 <- "hi"
c1 <- 3
c2 <- "hello"
close(c1)
close(c2)
}

互相发送

func Pingpong() {
i := 0
for {
fmt.Println(<-c)
c <- fmt.Sprintf("From Pingpong: Hi, #%d", i)
i++
}
}
func main() {
c = make(chan string)
go Pingpong()
for i := 0; i < 10; i++ {
c <- fmt.Sprintf("From main: Hello, #%d", i)
fmt.Println(<-c)
}
}