golang学习笔记
安装与配置
安装go
下载网址:
https://golang.google.cn/dl/
Windows包括32位和64位版本,zip是压缩包,Installer需要安装;Linux中.tar.gz表示解压后就能使用64位,Mac系统只有一个下载。
推荐直接在C盘根目录下新建一个Go目录,如C:\Go
,直接一路next即可。
等待安装完成后,在命令行中输入go version
可以检查一下是否安装成功:
将刚才安装go的目录下的bin文件夹加入系统变量的path中:
在命令行中输入以下两行代码,修改设置:
go env -w GO111MODULE="on"
go env -w GOPROXY="http://goproxy.cn"
安装git,全使用默认的安装选项即可:
https://www.git-scm.com/download/win
将git\bin
目录添加到环境变量中(系统环境变量的path),如果没有自动添加的话就手动添加一下。
vscode中配置go
- 装GO插件(Go Team at Google)
- 创建第一个GO文件,右下角会又弹窗,建议下载Go全部依赖包(关键),点击“install all” 下载全部如果没有弹窗,可以手动下载依赖包:
键盘输入:ctrl+shift+p
弹窗选择:Go:install/Update Tool
选择下载依赖包,建议全部选中下载
vscode运行方式(4种)
-
编译后执行
a.初始化:
执行:go mod init hello(项目名或者仓库名或其他)
结果:生成go.mod文件,这文件是描述项目包信息,项目包通过hello可以被引用
b.将hello.go编译成执行文件(3种方式):
执行1:go build,默认生成hello.exe
执行2:go build hello,生成hello.exe
执行3:go build -o world.exe,生成world.exe
c.运行:
文件目录下执行:hello.exe -
手动输入运行:
hello.go文件目录下执行:go run hello.go -
利用插件运行:
安装插件:Code Runner
点击右上角三角符Run Code 或者按F4 -
调试运行:
a.在launch.json文件中添加配置:
{
"name": "GO",
"type": "go",
"request": "launch",
"mode": "debug",
"debugAdapter": "legacy",
"program": "${file}"
}
b.按下F5,在调试控制台会显示结果
c.如果报错:couldn’t start dlv dap: connection timeout
那就重装依赖包:dlv dap
go的一些基础操作
几个路径解释
- GOROOT:GO的安装目录
- GOPATH:GO的工作目录
- bin:编译所形成的二进制文件
- pkg:编译后包文件存放位置
- src:源码位置,将自己clone下来的源代码放在此位置可以解决找不到包的情况
go常用命令
- build: 编译包和依赖
- env: 打印go的环境信息
- fmt: 运行gofmt进行格式化
- get: 下载并安装包和依赖
- install: 编译并安装包和依赖
- list: 列出包
- mod:模块管理
- run: 编译并运行go程序
- test: 运行测试
- version: 显示go的版本
go中的包
-
包可以区分命令空间(一个文件夹中不能有两个同名文件),也可以更好地管理项目。go中创建一个包一般是创建一个文件夹,在该文件夹里面的go文件中,使用
package
关键字声明包名称。通常文件夹名称和包名称相同
,并且同一个文件夹下面只有一个包。 -
当设置为
GO111MODULE=on
的时候,我们使用go mod来管理包。 -
GO111MODULE=off,无模块支持,go命令行将不会支持module功能,寻找依赖包的方式将会沿用旧版本那种通过vendor目录或GOPATH模式来查找
-
GO111MODULE=on,模块支持,go命令行会使用modules,而一点也不会去GOPATH目录下查找
-
GO111MODULE=auto,默认值,go命令行将会根据当前目录来决定是否启用module功能。这种情况下可以分为两种情形:
(1)当前目录在GOPATH/src之外且该目录包含go.mod文件,开启模块支持。
(2)当前文件在包含go.mod文件的目录下面 -
如果多个文件夹下有重名的package,它们其实是彼此无关的package。
-
如果一个go文件需要同时使用不同目录下的同名package,需要在import这些目录时为每个目录指定一个package的别名。
go mod tidy的作用
- 引用项目需要的依赖增加到go.mod文件。
- 去掉go.mod文件中项目不需要的依赖。
和包相关的报错
- no required module provides package github.com/gin-gonic/gin: go.mod file not found in current directory or any parent directory; see ‘go help modules’
在运行go程序时遇到此问题,发现如果是在终端使用go run
命令运行则没有报错,但在vscode中使用code-runner
插件则会报错,解决方案:
在settings.json文件中加入以下两行设置:
code-runner.runInTerminal": true, //这一行可以注释,也能解决问题
code-runner.fileDirectoryAsCwd": true
如果是在调试
go的时候出现这个问题,可能是因为调试的路径没有写对,修改一下launch.json
:
{
"name": "GoLaunch",
"type": "go",
"request": "launch",
"mode": "auto",
"debugAdapter": "legacy",
"program": "${workspaceRoot}", //此处设置改为"${file}"即可解决问题
"env": {},
"args": []
}
- could not import xxx(no required module xxx provides package)尝试在settings.json文件中加入设置:
"go.useLanguageServer": true,
使用go mod管理包
- 查看自己GOPATH的路径,比如我这里是
C:\GoProject
- cd到目标路径下,比如我要在
C:\GoProject\test
下建立一个go mod,那么就在C:\GoProject\test
输入go mod init test
此时即完成建立:
下载go第三方包
可以在这个网址找到包的路径:
https://pkg.go.dev/
go基础语法
hello world实例
package main
import "fmt"
func main() {
/* 这是我的第一个简单的程序 */
fmt.Println("Hello, World!")
}
- 第一行代码
package main
定义了包名。你必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main。package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。
下一行 import “fmt” 告诉 Go 编译器这个程序需要使用 fmt 包(的函数,或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数。 - 下一行
func main()
是程序开始执行的函数。main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。 - 下一行
func main()
是程序开始执行的函数。main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。 - 下一行是注释,在程序执行时将被忽略。单行注释是最常见的注释形式,你可以在任何地方使用以 // 开头的单行注释。多行注释也叫块注释,均已以 /* 开头,并以 */ 结尾,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段。
- 下一行 fmt.Println(…) 可以将字符串输出到控制台,并在最后自动增加换行字符 \n。
使用 fmt.Print(“hello, world\n”) 可以得到相同的结果。
Print 和 Println 这两个函数也支持使用变量,如:fmt.Println(arr)。如果没有特别指定,它们会以默认的打印格式将变量 arr 输出到控制台。 - 当标识符(包括常量、变量、类型、函数名、结构字段等等)
以一个大写字母开头
,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头
,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected )。
短变量声明
语法:变量名 := 数据
注意:只能在当前函数作用域中使用,不能作为全局变量
var global_variable string = "666"
func main() {
short_variable := "777"
fmt.Println(global_variable)
fmt.Println(short_variable)
}
匿名变量
如果我们接收到多个变量,有一些变量使用不到,可以使用下划线_
表示变量名称,这种变量叫做匿名变量:
func getNameAndAge() (string, int) {
return "tom", 20
}
func main() {
_, age := getNameAndAge()
fmt.Println(age)
}
iota
iota,特殊常量,可以认为是一个可以被编译器修改的常量。
iota 在 const关键字出现时将被重置为 0
(const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。
func main() {
const (
a1 = iota
a2 = iota
a3 = iota
)
fmt.Printf("a1: %v\n", a1)
fmt.Printf("a2: %v\n", a2)
fmt.Printf("a3: %v\n", a3)
}
运行结果:
a1: 0
a2: 1
a3: 2
go中println和printf的用法区别
Println
: 可以打印出字符串,和变量
Printf
: 只可以打印出格式化的字符串,可以输出字符串类型的变量
package main
import "fmt"
func main() {
a := 1
b := 'a'
c := 3.14
d := "abc"
fmt.Println("a =", a)
fmt.Println("b = ", b)
fmt.Println("c=", c)
fmt.Println("d=", d)
fmt.Printf("a=%d\nb=%v\nc=%v\nd=%s\n", a, b, c, d)
}
格式化操作:
动词 | 功能 |
---|---|
%v | 按值的本来值输出 |
%+v | 如果值是一个结构体,%+v 的格式化输出内容将包括结构体的字段名 |
%T | 打印值的类型 |
%b | 输出二进制表示形式 |
%d | 输出十进制表示形式 |
%x | 输出十六进制表示形式 |
%p | 十六进制表示,前缀0x的内存地址 |
go中的数据类型
int
和uint
在32位操作系统上,它们均使用32位(4个字节),在64位操作系统上,它们均使用64位(8个字节)。uintptr
的长度被设定位足够存放一个指针即可。int
是计算最快的一种类型。- 整形的零值为0,浮点型的零值为0.0。
类型 | 描述 |
---|---|
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) |
package main
import (
"fmt"
"math"
"unsafe"
)
func main() {
var i8 int8
var i16 int16
var i32 int32
var i64 int64
var ui8 uint8
var ui16 uint16
var ui32 uint32
var ui64 uint64
fmt.Printf("%T %dB %v~%v\n", i8, unsafe.Sizeof(i8), math.MinInt8, math.MaxInt8)
fmt.Printf("%T %dB %v~%v\n", i16, unsafe.Sizeof(i16), math.MinInt16, math.MaxInt16)
fmt.Printf("%T %dB %v~%v\n", i32, unsafe.Sizeof(i32), math.MinInt32, math.MaxInt32)
fmt.Printf("%T %dB %v~%v\n", i64, unsafe.Sizeof(i64), math.MinInt64, math.MaxInt64)
fmt.Printf("%T %dB %v~%v\n", ui8, unsafe.Sizeof(ui8), 0, math.MaxUint8)
fmt.Printf("%T %dB %v~%v\n", ui16, unsafe.Sizeof(ui16), 0, math.MaxUint16)
fmt.Printf("%T %dB %v~%v\n", ui32, unsafe.Sizeof(ui32), 0, math.MaxUint32)
fmt.Printf("%T %dB %v~%v\n", ui64, unsafe.Sizeof(ui64), 0, uint64(math.MaxUint64))
}
输出结果:
int8 1B -128~127
int16 2B -32768~32767
int32 4B -2147483648~2147483647
int64 8B -9223372036854775808~9223372036854775807
uint8 1B 0~255
uint16 2B 0~65535
uint32 4B 0~4294967295
uint64 8B 0~18446744073709551615
go中的if语句
- 不能使用
0
或非0
表示真假 - 不需要使用括号将
条件包含起来
- 大括号
{}
必须存在,即便只有一行语句 - 左括号必须在
if
或else
的同一行 - 在
if
之后,条件语句之前,可以添加变量初始化语句,使用;
分割
func main() {
// var a bool = true
if age := 20; age > 6 {
fmt.Println("666")
}
}
for range循环
Go中可以使用for range
遍历数组、切片、字符串、map及通道(channel),通过for range
遍历的返回值有以下规律:
- 数组、切片、字符串返回
索引和值
- map返回
键和值
- 通道(channel)只返回通道内的值
func main() {
var a = [5]int{1, 2, 3, 4, 5}
for i, v := range a {
fmt.Printf("i:%d , v:%v\n", i, v)
}
m1 := map[string]string{"name": "An", "age": "20"}
for k, v := range m1 {
fmt.Printf("%v: %v\n", k, v)
}
}
continue 语句
Go 语言的 continue 语句 有点像 break 语句。但是 continue 不是跳出循环,而是跳过当前循环执行下一次循环语句。for 循环中,执行 continue 语句会触发 for 增量语句的执行。在多重循环中,可以用标号 label 标出想 continue 的循环。
func main() {
for i := 0; i < 10; i++ {
LABEL:
for j := 0; j < 10; j++ {
if i == 2 && j == 2 {
continue LABEL
}
fmt.Printf("%v,%v\n", i, j)
}
}
}
输出结果:
数组
- 数组是
相同数据类型
的一组数据的集合,数组一旦定义长度则不能修改
,数组可以通过下标(索引)
来访问元素。 - 定义一个数组:
var variable_name [SIZE] variable_type
,如var a1[2]int
、var a1 = [3]int{1, 2}
、var a2 = [3]string{"A", "B", "C"}
- 数组长度可以省略,使用
...
代替,如var a3 = [...]int{1, 2, 3}
遍历一个数组:
func main() {
var a1 = [3]int{1, 2, 3}
//根据下标和长度遍历
for i := 0; i < len(a1); i++ {
fmt.Printf("a1[%v]: %v\n", i, a1[i])
}
//使用for range遍历
for i, v := range a1 {
fmt.Printf("a1[%v]: %v\n", i, v)
}
}
切片
- 可以把切片理解为可变长度的数组,它底层就是用数组实现的,增加了自动扩容功能,切片(
Slice
)是一个拥有相同类型元素的可变长度的序列。 - 声明一个切片和声明一个数组类似,只要不添加长度就可以了:
var identifier []type
。 - 切片是引用类型,也可以使用
make
函数来创建切片:var slice1 []type = make([]type,len)
或slice1 := make([]type,len)
。 - 还可以使用数组进行切片的初始化:
arr := [...]int {1,2,3}; s1 := arr[:]
- 切片的添加与删除,如下:
- 添加
func main() {
var s1 = []int{}
s1 = append(s1, 100)
s1 = append(s1, 200)
s1 = append(s1, s1[:2]...)
s1 = append(s1, s1[2:]...)
fmt.Printf("s1: %v\n", s1)
}
- 删除
func main() {
// 要从切片a中删除索引为index的元素,操作方法是 a = append(a[:index], a[index+1:]...)
var s1 = []int{1, 2, 3, 4, 5}
s1 = append(s1[:2], s1[3:]...) //删除索引为2的元素
fmt.Printf("s1: %v\n", s1)
}
- 更新(修改)
func main() {
var s1 = []int{1, 2, 3, 4, 5}
s1[1] = 100
fmt.Printf("s1: %v\n", s1)
}
map
Map是一种key:value
键值对的数据结构容器。Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,我们无法决定它的返回顺序,这是因为 Map 是使用 hash 表来实现的。
- 创建map:
// 1.声明变量
var map_variable map[key_data_type]value_data_type
// 2.使用make函数
map_variable := make(map[key_data_type]value_data_type)
func main() {
m1 := make(map[string]string)
m1["name"] = "tom"
m1["age"] = "20"
fmt.Printf("m1: %v\n", m1)
m2 := map[string]string{
"name": "xx",
"age": "24",
}
fmt.Printf("m2: %v\n", m2)
}
创建一个复杂的map:
package main
import "fmt"
type Person struct {
age, height int
}
func main() {
k := map[string]Person{
"Peter": {23, 170},
"Tom": {24, 180},
"Green": {age: 25},
"Q": {height: 50},
}
fmt.Printf("k: %v\n", k) //输出结果 k: map[Green:{25 0} Peter:{23 170} Q:{0 50} Tom:{24 180}]
}
- go中的特殊用法,判断某个键是否存在
func main() {
var m1 = map[string]string{"name": "xx", "age": "20"}
var k1 = "name"
var k2 = "email"
v, ok := m1[k1]
fmt.Printf("v: %v\n", v)
fmt.Printf("ok: %v\n", ok)
fmt.Println("---------------------")
v, ok = m1[k2]
fmt.Printf("v: %v\n", v)
fmt.Printf("ok: %v\n", ok)
fmt.Println("---------------------")
}
输出结果:
v: xx
ok: true
---------------------
v:
ok: false
---------------------
函数
- go中函数的特性
- go中有三种函数:普通函数、匿名函数和方法(定义在struct上的函数)
- go中没有函数重载(overload)
- go中的函数不能嵌套函数,但可以嵌套匿名函数
- 函数是一个值,可以将函数赋值给变量,使得这个变量也成为函数
- 函数可以作为参数传递给另一个函数
- 函数的返回值可以是一个函数
- 函数调用的时候,如果有参数传递给函数,则先拷贝参数的副本,再讲副本传递给函数
- 函数参数可以没有名称
- 函数在使用之前必须先定义,可以调用函数来完成某个任务
- 定义函数的语法
func function_name( [parameter list] ) [return_types] {
函数体
}
func
:函数由 func 开始声明function_name
:函数名称,参数列表和返回值类型构成了函数签名。parameter list
:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。return_types
:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。- 函数体:函数定义的代码集合。
- 还可以使用
type
关键字来定义一个函数类型,语法格式如下:
type fun func(int,int) int
type fun func(int, int) int
func sum(a int, b int) int {
return a + b
}
func main() {
var f fun = sum
s := f(1, 2)
fmt.Printf("s: %v\n", s)
}
- 把函数作为参数或返回值:
func sum(a int, b int) int {
return a + b
}
func sub(a int, b int) int {
return a - b
}
func main() {
ff := cal("+")
f := ff(5, 2)
fmt.Printf("f: %v\n", f)
}
func cal(operator string) func(int, int) int {
switch operator {
case "+":
return sum
case "-":
return sub
default:
return nil
}
}
- 匿名函数
- go中函数不能嵌套,但在函数内部可以定义匿名函数实现简单的功能调用,定义匿名函数的语法格式:
func (参数列表)(返回值)
func main() {
max := func(a int, b int) int {
if a > b {
return a
} else {
return b
}
}
i := max(5, 2)
fmt.Printf("i: %v\n", i)
//自己调用自己
i2 := func(a int, b int) int {
if a > b {
return a
} else {
return b
}
}(5, 6)
fmt.Printf("i2: %v\n", i2)
}
闭包
闭包可以理解为定义在一个函数内部的函数
,在本质上闭包是将函数内部和函数外部连接起来的桥梁,或者说是函数和其引用环境的组合体:闭包=函数+引用环境
。
func add() func(int) int {
var x int
return func(y int) int {
x += y
return x
}
}
func main() {
var f = add()
fmt.Println(f(10))
fmt.Println(f(20))
fmt.Println(f(30))
}
---------------------------------------------------
输出结果:
10
30
60
func makeSuffixFunc(suffix string) func(string) string {
return func(name string) string {
if !strings.HasSuffix(name, suffix) {
return name + suffix
}
return name
}
}
func main() {
jpgFunc := makeSuffixFunc(".jpg")
txtFunc := makeSuffixFunc(".txt")
fmt.Println(jpgFunc("test"))
fmt.Println(txtFunc("test"))
}
---------------------------------------------------
输出结果:
test.jpg
test.txt
defer语句
defer
语句会将其后面跟随的语句进行延迟
处理,在defer
归属的函数即将返回时,将延迟处理的语句按defer
定义的逆序进行执行,也就是说先被defer
的语句最后被执行,最后被defer
的语句最先被执行。
func main() {
fmt.Println("start")
defer fmt.Println("step1")
defer fmt.Println("step2")
defer fmt.Println("step3")
fmt.Println("end")
}
init函数
- 先于
main
函数执行,实现包级别的一些初始化
操作。 - 主要特点:
- 先于main函数执行,不能被其他函数调用
- 没有输入参数与返回值
- 每个包可以有多个init函数
- 包的每个源文件也可以有多个init函数
- 同一个包的init执行顺序go中没有明确定义
- 不同包的init函数按照包导入的依赖关系决定执行顺序
指针
- go中的函数传参都是值拷贝,当我们想要修改某个变量的时候,我们可以创建一个指向该变量地址的指针变量,传递数据使用指针,而无需拷贝数据。
- 类型指针不能进行偏移和运算。
&
取地址,*
根据地址取值。- 指针声明的语法:
var var_name *var_type
func main() {
var ip *int
fmt.Printf("ip: %v\n", ip) //<nil>
fmt.Printf("ip: %T\n", ip) //*int
var i int = 100
ip = &i
fmt.Printf("ip: %v\n", ip) //0xc00000a0f0
fmt.Printf("ip: %v\n", *ip) //100
}
- 指向数组的指针:
var ptr [MAX]*int
表示数组里面的元素的类型是指针类型
const MAX int = 3
func main() {
a := []int{1, 3, 5}
var ptr [MAX]*int
fmt.Println(ptr) //输出结果 [<nil> <nil> <nil>]
for i := 0; i < MAX; i++ {
ptr[i] = &a[i] //整数地址赋值给指针数组
}
fmt.Printf("ptr: %v\n", ptr) //输出结果 ptr: [0xc000014180 0xc000014188 0xc000014190]
for i := 0; i < MAX; i++ {
fmt.Printf("a[%d] = %d\n", i, *ptr[i]) //*ptr[i]就是打印出相关指针的值
}
}
---------------------------------------------------
输出结果:
[<nil> <nil> <nil>]
ptr: [0xc000014180 0xc000014188 0xc000014190]
a[0] = 1
a[1] = 3
a[2] = 5
结构体
- go中没有面向对象的概念,但是可以用结构体来实现面向对象的一些特性,例如继承、组合等特性。
- 未初始化的结构体,成员都是零值,int对应0,float对应0.0,bool对应false,string对应""
- 语法结构如下:
type struct_variable_type struct {
member definition
}
//type 结构体定义关键字
//struct_variable_type 结构体类型名称
//struct 结构体定义关键字
//member definition 成员定义
func main() {
var tom Person
tom.id = 666
tom.name = "tome"
fmt.Printf("tom: %v\n", tom) //输出 tom: {666 0 tome}
kite := Person{}
fmt.Printf("kite: %v\n", kite) //输出 kite: {0 0 }
}
type Person struct {
id, age int
name string
}
- 匿名结构体:结构体临时使用的情况下可以不起名字
func main() {
var dog struct {
id int
name string
}
dog.id = 1
dog.name = "花花"
fmt.Printf("dog: %v\n", dog) //输出结果 dog: {1 花花}
}
- 对结构体进行初始化
func main() {
type Person struct {
id, age int
name, email string
}
var tom Person
fmt.Printf("tom: %v\n", tom) //输出结果 tom: {0 0 }
tom = Person{
id: 101,
name: "tom",
age: 20,
email: "888@qq.com",
}
fmt.Printf("tom: %v\n", tom) //输出结果 tom: {101 20 tom 888@qq.com}
//使用new关键字初始化
var Jim = new(Person)
}
- 结构体指针
func main() {
type Person struct {
id, age int
name, email string
}
var tom Person = Person{
id: 101,
name: "tom",
age: 20,
email: "888@qq.com",
}
var p_person *Person = &tom
fmt.Printf("p_person: %v\n", *p_person) //输出结果 p_person: {101 20 tom 888@qq.com}
fmt.Printf("p_person: %p\n", p_person) //输出结果 p_person: 0xc0000783c0
}
- 嵌套结构体
type Dog struct {
name string
color string
age int
}
type Person struct {
dog Dog
name string
age int
}
func main() {
var tom Person
tom.dog.name = "花花"
tom.dog.color = "黄色"
tom.dog.age = 5
tom.name = "tom"
tom.age = 30
fmt.Printf("tom: %v\n", tom) //输出结果 tom: {{花花 黄色 5} tom 30}
}
方法
- Go中的方法是一种
特殊的函数
,定义于struct之上,与struct关联、绑定,被称为struct的接受者(receiver)。通俗讲,方法就是有接受者的函数。 - 定义方法的语法如下:
type mytype struct{}
func (recv mytype) my_method(para) return_type{}
func (recv *mytype) my_method(para) return_type{}
// mytype 定义一个结构体
// recv 接受该方法的结构体
// my_method 方法名称
// para 参数列表
// return_type 返回值类型
type Person struct {
name string
}
func (per Person) eat() {
fmt.Printf("%v,eat...", per.name)
}
func main() {
per := Person{
name: "tom",
}
per.eat() //输出结果 tom,eat...
}
type Customer struct {
name string
}
func (customer Customer) login(name string, pwd string) bool {
fmt.Printf("customer.name:%v\n", customer.name)
if name == "tom" && pwd == "123" {
return true
} else {
return false
}
}
func main() {
cus := Customer{
name: "tom",
}
b := cus.login("tom", "666")
fmt.Printf("b: %v\n", b)
}
------------------------------------------
输出结果:
customer.name:tom
b: false
- 方法的receiver type 并非一定要是struct类型
- struct结合它的方法就等价于面向对象中的类,只不过struct可以和它的方法分开,并非一定要属于同一个文件,但必须属于同一个包
- 方法有两种接受类型:
(T Type)
和(T *Type)
,他们之间有区别 - 如果receiver是一个指针类型,则会自动解除引用
- 方法和type是分开的,意味着实例的行为和数据存储是分开的,但是它们通过receiver建立起关联关系
type Person struct {
name string
}
func main() {
p1 := Person{name: "tom"}
fmt.Printf("p1: %T\n", p1) //输出结果 p1: main.Person
p2 := &Person{name: "tom"} //输出结果 p2: *main.Person
fmt.Printf("p2: %T\n", p2)
}
type Person struct {
name string
}
func main() {
p1 := Person{
name: "tom",
}
p2 := &Person{
name: "tom",
}
fmt.Printf("p1: %v\n", p1) //输出结果 p1: {tom}
showPerson1(p1)
fmt.Printf("p1: %v\n", p1) //输出结果 p1: {tom}
fmt.Printf("p2: %v\n", p2) //输出结果 p2: &{tom}
showPerson2(p2)
fmt.Printf("p2: %v\n", p2) //输出结果 p2: &{Jake}
}
func showPerson1(per Person) {
per.name = "Jake"
}
func showPerson2(per *Person) {
per.name = "Jake"
}
从运行结果看,p1是值传递,拷贝了副本,地址发生了改变,而p2是指针类型,地址没有发生改变。
接口
- 接口是一种新的类型定义,它把所有具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。
/* 定义接口 */
type interface_name interface {
method_name1 [return_type]
method_name2 [return_type]
method_name3 [return_type]
...
method_namen [return_type]
}
/* 定义结构体 */
type struct_name struct {
/* variables */
}
/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
/* 方法实现 */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
/* 方法实现*/
}
func main() {
c := Computer{
name: "联想",
}
c.read()
c.write()
}
type USB interface {
read()
write()
}
type Computer struct {
name string
}
func (c Computer) read() {
fmt.Printf("c.name: %v\n", c.name)
fmt.Println("read...")
}
func (c Computer) write() {
fmt.Printf("c.name: %v\n", c.name)
fmt.Println("write...")
}
- 一个类型可以实现多个接口
type Player interface {
playMusic()
}
type Video interface {
playVideo()
}
type Mobile struct {
}
func (m Mobile) playMusic() {
fmt.Println("play music")
}
func (m Mobile) playVideo() {
fmt.Println("play video")
}
func main() {
m := Mobile{}
m.playMusic()
m.playVideo()
}
- 多个类型实现同一个接口(类似于
多态
)
type Pet interface {
eat()
}
type Dog struct {
}
type Cat struct {
}
func (cat Cat) eat() {
fmt.Println("cat eat...")
}
func (dog Dog) eat() {
fmt.Println("dog eat...")
}
func main() {
var p Pet
p = Cat{}
p.eat()
p = Dog{}
p.eat()
}
- 接口的组合(嵌套)
type Flyer interface {
fly()
}
type Swimmer interface {
swim()
}
type FlyFish interface {
Flyer
Swimmer
}
type Fish struct {
}
func (fish Fish) fly() {
fmt.Println("fly...")
}
func (fish Fish) swim() {
fmt.Println("swim...")
}
func main() {
var ff FlyFish = Fish{}
ff.fly()
ff.swim()
}
继承
go本质上没有继承的概念,但可以通过结构体嵌套
来实现这个特性。
type Animal struct {
name string
age int
}
func (a Animal) eat() {
fmt.Println("eat...")
}
func (a Animal) sleep() {
fmt.Println("sleep...")
}
type Dog struct {
Animal
}
type Cat struct {
Animal
}
func main() {
dog := Dog{
Animal{
name: "dog",
age: 2,
},
}
cat := Cat{
Animal{
name: "cat",
age: 3,
},
}
dog.eat()
dog.sleep()
cat.eat()
cat.sleep()
}
构造函数
go中没有构造函数的概念,但可以使用函数来模拟构造函数的功能
type Person struct {
name string
age int
}
func NewPerson(name string, age int) (*Person, error) {
if name == "" {
return nil, fmt.Errorf("name 不能为空")
}
if age < 10 {
return nil, fmt.Errorf("age 不能小于10")
}
return &Person{name: name, age: age}, nil
}
func main() {
person, err := NewPerson("tom", 1)
if err == nil {
fmt.Printf("person:%v\n", *person)
} else {
fmt.Printf("err:%v\n", err)
}
}
错误处理:panic、recover与defer
Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。
package main
import "fmt"
func main(){
fmt.Println("c")
defer func(){ // 必须要先声明defer,否则不能捕获到panic异常
fmt.Println("d")
if err:=recover();err!=nil{
fmt.Println(err) // 这里的err其实就是panic传入的内容:"异常信息"
}
fmt.Println("e")
}()
f() //开始调用f
fmt.Println("f") //这里开始下面代码不会再执行
}
func f(){
fmt.Println("a")
panic("异常信息")
fmt.Println("b") //这里开始下面代码不会再执行
fmt.Println("f")
}
-------------------------------------------------------------------
输出结果:
c
a
d
异常信息
e
go常用方法
读取文件
content, err := os.ReadFile("hh.json")
if err != nil {
panic(err)
}
写入文件
var d1 = []byte("hello world")
err := ioutil.WriteFile("./output2.txt", d1, 0666) //写入文件(字节数组)
if err != nil {
panic(err)
}
获取随机数
go并发编程
协程
go中的并发是函数
相互独立运行的能力,goroutines
是并发运行的函数,在go中创建一个协程非常简单,就是在一个任务函数前面添加一个go关键字
:go task()
。
package main
import (
"fmt"
"time"
)
func show(msg string) {
for i := 0; i < 5; i++ {
fmt.Printf("msg: %v\n", msg)
time.Sleep(time.Millisecond * 100)
}
}
func main() {
go show("java")
show("golang")
}
通道channel
- Go中提供了被称为通道的机制,用于在goroutine之间
共享数据
。 - 根据数据交换的行为,有两种类型的通道:
无缓冲通道和缓冲通道
。无缓冲通道用于执行goroutine之间的同步通信,而缓冲通道用于执行异步通信。无缓冲通道保证在发送和接收发生的瞬间执行两个goroutine之间的交换,缓冲通道没有这样的保证。 - 通道由
make
函数创建,该函数指定chan
关键字和通道的元素类型。 无缓冲通道
:在无缓冲通道中,在接收到任何值之前没有能力保存它。在这种类型的通道中,发送和接收goroutine在任何发送或接收操作完成之前的同一时刻都准备就绪。如果两个goroutine没有在同一时刻准备好,则通道会让执行其各自发送或接收操作的goroutine首先等待。同步是通道上发送和接收之间交互的基础。没有另一个就不可能发生。缓冲通道
:在缓冲通道中,有能力在接收到一个或多个值之前保存它们。在这种类型的通道中,不要强制goroutine在同一时刻准备好执行发送和接收。当发送和接收阻塞时也有不同的条件。只有当通道中没有要接收的值时,接收才会阻塞。仅当没有可用缓冲区来放置正在发送的值时,发送才会阻塞。
创建无缓冲和缓冲通道的代码块:- 通道的发送和接收特性:
- 对于同一个通道,发送操作之间是互斥的。接收操作之间也是互斥的。
- 发送操作和接收操作中对元素值的处理都是不可分割的。
- 发送操作在完全完成之前会被阻塞。接收操作也是如此。
unbuffered := make(chan int) //整型无缓冲通道
buffered := make(chan int,10) //整型有缓冲通道
将值发送到通道的代码块需要使用<-
运算符,一个包含5个值的缓冲区的字符串类型的goroutine1通道:
goroutine1 := make(chan string ,5) //字符串缓冲通道
goroutine1 <- "hello" //通过通道发送字符串
从通道接收值:
data := <- goroutine1 //从通道接收字符串
例子1:
package main
import (
"fmt"
"math/rand"
"time"
)
// 创建int类型通道,只能传入int类型值
var values = make(chan int)
func send() {
rand.Seed(time.Now().UnixNano())
value := rand.Intn(10)
fmt.Printf("send: %v\n", value)
// time.Sleep(time.Second * 5)
values <- value
}
func main() {
// 从通道接收值
defer close(values)
go send()
fmt.Println("wait...")
value := <-values
fmt.Printf("receive: %v\n", value)
fmt.Println("end...")
}
例子2:
ch1 := make(chan int) //创建一个整型类型的通道
ch2 := make(chan interface{}) //创建一个空接口类型的通道, 可以存放任意格式
type Equip struct{ /* 一些字段 */ }
ch2 := make(chan *Equip) //创建Equip指针类型的通道, 可以存放*Equip
package main
import "fmt"
type Product struct {
full_name string
sale_start_date string
is_sale bool
}
func main() {
structchan := make(chan *Product, 5)
t1 := Product{
full_name: "a124",
sale_start_date: "2031-12-11",
is_sale: true,
}
structchan <- &t1
close(structchan)
t2 := <-structchan
fmt.Printf("t2: %v\n", t2) //输出结果 t2: &{a124 2031-12-11 true}
}
参考
golang入门到项目实战 [2022最新Go语言教程,没有废话,纯干货!]
Go-常用命令go的使用(build、env、run、fmt等)
Go语言-Golang Gin Go Web Gorm Go-Micro微服务教程
Go panic 用法
Channel 通道详解
一看就懂系列之Golang的goroutine和通道