go基础第一遍学习记录,以下内容来自李文周博客
https://blog.csdn.net/Fe_cow/article/details/103804689
首先 Println 跟 Printf 都是fmt包中的公共方法。
Println:打印字符串
、变量
;
Printf:打印需要格式化的字符串
,可以输出字符串类型的变量
;不可以输出整型变量和整型;
简单理解,当需要格式化输出信息时,一般选择Printf,其余使用Println
。
%v:默认方式打印变量的值;
%T:打印变量的类型;
%+d:带符号的整型;
%q:打印单引号;
%o: 不带零的八进制;
%#o: 带零的八进制;
%x: 小写的十六进制;
%X: 大写的十六进制;
%#x: 带0x的十六进制;
%U: 打印Unicode字符;
%#U: 打印带字符的Unicode;
%b: 打印整型的二进制;
%f:6位小数点;
%.6f: 6位小数点;
%e:6位小数点(科学计数法);
%g:最少的数字来表示;1
%.3g:最多3位数字
表示;
i := 12.123456789
fmt.Printf(“i: %.3g”,i)
// 输出结果
12.1
%.3f:最多3位小数
表示;
i := 12.123456789
fmt.Printf(“i: %.3g”,i)
// 输出结果
12.123
%s:正常输出字符串;
%q: 字符串带双引号,字符串中的引号带转义符;
%#q: 字符串带反引号,如果字符串内有反引号,就用双引号代替;
%x: 将字符串转换为小写的16进制格式;
%X: 将字符串转换为大写的16进制格式;
% x: 带空格的16进制格式;
%t:打印true 或 false;
mygo基础
print,printf,println:
函数 | 同函数输出多项 | 不同函数输出 |
---|---|---|
Println | 之间存在空格 | 换行 |
不存在空格 | 不换行 | |
Printf | 格式化输出 | 不换行 |
占位符d、b、o、x、X==
package main
import "fmt"
import "os"
type point struct {
a, b int
}
func main() {
//Go 为常规 Go 值的格式化设计提供了多种打印方式。
p := point{1, 2}
fmt.Printf("%v\n", p) // {1 2}
//如果值是一个结构体,%+v 的格式化输出内容将包括结构体的字段名。
fmt.Printf("%+v\n", p) // {x:1 y:2}
//%#v 形式则输出这个值的 Go 语法表示。例如,值的运行源代码片段。
fmt.Printf("%#v\n", p) // main.point{x:1, y:2}
//需要打印值的类型,使用 %T。
fmt.Printf("%T\n", p) // main.point
//格式化布尔值是简单的。
fmt.Printf("%t\n", true)
//格式化整形数有多种方式,使用 %d进行标准的十进制格式化。
fmt.Printf("%d\n", 123)
//这个输出二进制表示形式。
fmt.Printf("%b\n", 14)
//这个输出给定整数的对应字符。
fmt.Printf("%c\n", 33)
//%x 提供十六进制编码。
fmt.Printf("%x\n", 456)
//对于浮点型同样有很多的格式化选项。使用 %f 进行最基本的十进制格式化。
fmt.Printf("%f\n", 78.9)
//%e 和 %E 将浮点型格式化为(稍微有一点不同的)科学技科学记数法表示形式。
fmt.Printf("%e\n", 123400000.0)
fmt.Printf("%E\n", 123400000.0)
//使用 %s 进行基本的字符串输出。
fmt.Printf("%s\n", "\"string\"")
//像 Go 源代码中那样带有双引号的输出,使用 %q。
fmt.Printf("%q\n", "\"string\"")
//和上面的整形数一样,%x 输出使用 base-16 编码的字符串,每个字节使用 2 个字符表示。
fmt.Printf("%x\n", "hex this")
//要输出一个指针的值,使用 %p。
fmt.Printf("%p\n", &p)
//当输出数字的时候,你将经常想要控制输出结果的宽度和精度,可以使用在 % 后面使用数字来控制输出宽度。默认结果使用右对齐并且通过空格来填充空白部分。
fmt.Printf("|%6d|%6d|\n", 12, 345)
//你也可以指定浮点型的输出宽度,同时也可以通过 宽度.精度 的语法来指定输出的精度。
fmt.Printf("|%6.2f|%6.2f|\n", 1.2, 3.45)
//要最对齐,使用 - 标志。
fmt.Printf("|%-6.2f|%-6.2f|\n", 1.2, 3.45)
//你也许也想控制字符串输出时的宽度,特别是要确保他们在类表格输出时的对齐。这是基本的右对齐宽度表示。
fmt.Printf("|%6s|%6s|\n", "foo", "b")
//要左对齐,和数字一样,使用 - 标志。
fmt.Printf("|%-6s|%-6s|\n", "foo", "b")
//到目前为止,我们已经看过 Printf了,它通过 os.Stdout输出格式化的字符串。Sprintf 则格式化并返回一个字符串而不带任何输出。
s := fmt.Sprintf("a %s", "string")
fmt.Println(s)
//你可以使用 Fprintf 来格式化并输出到 io.Writers而不是 os.Stdout。
fmt.Fprintf(os.Stderr, "an %s\n", "error")
}
golang 的fmt 包实现了格式化I/O函数,类似于C的 printf 和 scanf。
# 定义示例类型和变量
type Human struct {
Name string
}
var people = Human{Name:"zhangsan"}
普通占位符
占位符 说明 举例 输出
%v 相应值的默认格式。 Printf("%v", people) {zhangsan},
%+v 打印结构体时,会添加字段名 Printf("%+v", people) {Name:zhangsan}
%#v 相应值的Go语法表示 Printf("#v", people) main.Human{Name:"zhangsan"}
%T 相应值的类型的Go语法表示 Printf("%T", people) main.Human
%% 字面上的百分号,并非值的占位符 Printf("%%") %
布尔占位符
占位符 说明 举例 输出
%t true 或 false。 Printf("%t", true) true
整数占位符
占位符 说明 举例 输出
%b 二进制表示 Printf("%b", 5) 101
%c 相应Unicode码点所表示的字符 Printf("%c", 0x4E2D) 中
%d 十进制表示 Printf("%d", 0x12) 18
%o 八进制表示 Printf("%d", 10) 12
%q 单引号围绕的字符字面值,由Go语法安全地转义 Printf("%q", 0x4E2D) '中'
%x 十六进制表示,字母形式为小写 a-f Printf("%x", 13) d
%X 十六进制表示,字母形式为大写 A-F Printf("%x", 13) D
%U Unicode格式:U+1234,等同于 "U+%04X" Printf("%U", 0x4E2D) U+4E2D
浮点数和复数的组成部分(实部和虚部)
占位符 说明 举例 输出
%b 无小数部分的,指数为二的幂的科学计数法,
与 strconv.FormatFloat 的 'b' 转换格式一致。例如 -123456p-78
%e 科学计数法,例如 -1234.456e+78 Printf("%e", 10.2) 1.020000e+01
%E 科学计数法,例如 -1234.456E+78 Printf("%e", 10.2) 1.020000E+01
%f 有小数点而无指数,例如 123.456 Printf("%f", 10.2) 10.200000
%g 根据情况选择 %e 或 %f 以产生更紧凑的(无末尾的0)输出 Printf("%g", 10.20) 10.2
%G 根据情况选择 %E 或 %f 以产生更紧凑的(无末尾的0)输出 Printf("%G", 10.20+2i) (10.2+2i)
字符串与字节切片
占位符 说明 举例 输出
%s 输出字符串表示(string类型或[]byte) Printf("%s", []byte("Go语言")) Go语言
%q 双引号围绕的字符串,由Go语法安全地转义 Printf("%q", "Go语言") "Go语言"
%x 十六进制,小写字母,每字节两个字符 Printf("%x", "golang") 676f6c616e67
%X 十六进制,大写字母,每字节两个字符 Printf("%X", "golang") 676F6C616E67
指针
占位符 说明 举例 输出
%p 十六进制表示,前缀 0x Printf("%p", &people) 0x4f57f0
其它标记
占位符 说明 举例 输出
+ 总打印数值的正负号;对于%q(%+q)保证只输出ASCII编码的字符。
Printf("%+q", "中文") "\u4e2d\u6587"
- 在右侧而非左侧填充空格(左对齐该区域)
# 备用格式:为八进制添加前导 0(%#o) Printf("%#U", '中') U+4E2D
为十六进制添加前导 0x(%#x)或 0X(%#X),为 %p(%#p)去掉前导 0x;
如果可能的话,%q(%#q)会打印原始 (即反引号围绕的)字符串;
如果是可打印字符,%U(%#U)会写出该字符的
Unicode 编码形式(如字符 x 会被打印成 U+0078 'x')。
' ' (空格)为数值中省略的正负号留出空白(% d);
以十六进制(% x, % X)打印字符串或切片时,在字节之间用空格隔开
0 填充前导的0而非空格;对于数字,这会将填充移到正负号之后
golang没有 ‘%u’ 点位符,若整数为无符号类型,默认就会被打印成无符号的。
宽度与精度的控制格式以Unicode码点为单位。宽度为该数值占用区域的最小宽度;精度为小数点之后的位数。
操作数的类型为int时,宽度与精度都可用字符 ‘*’ 表示。
对于 %g/%G 而言,精度为所有数字的总数,例如:123.45,%.4g 会打印123.5,(而 %6.2f 会打印123.45)。
%e 和 %f 的默认精度为6
对大多数的数值类型而言,宽度为输出的最小字符数,如果必要的话会为已格式化的形式填充空格。
而以字符串类型,精度为输出的最大字符数,如果必要的话会直接截断。
属于编译型语言,执行顺序比解释型语言少一步,优于解释性语言
语法简单
代码风格统一
go fmt
。开发效率高
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f8mDduHR-1655435532034)(go基础.assets/image-20220615105857692.png)]
GOROOT
和GOPATH
都是环境变量,其中GOROOT
是我们安装go开发包的路径,而从Go 1.8版本开始,Go开发包在安装完成后会为GOPATH
设置一个默认目录,并且在Go1.14及之后的版本中启用了Go Module模式之后,不一定非要将代码写到GOPATH目录下,所以也就不需要我们再自己配置GOPATH了,使用默认的即可。
Go1.14版本之后,都推荐使用go mod
模式来管理依赖环境了,也不再强制我们把代码必须写在GOPATH
下面的src目录了,你可以在你电脑的任意位置编写go代码。(网上有些教程适用于1.11版本之前。)
默认GoPROXY配置是:GOPROXY=https://proxy.golang.org,direct
,由于国内访问不到https://proxy.golang.org
,所以我们需要换一个PROXY,这里推荐使用https://goproxy.io
或https://goproxy.cn
。
可以执行下面的命令修改GOPROXY:
go env -w GOPROXY=https://goproxy.cn,direct
初始化项目,生成go.mod
文件
go mod init 项目名
创建main.go文件,编写main.go
代码
package main // 声明 main 包,表明当前是一个可执行程序
import "fmt" // 导入内置 fmt 包
func main(){ // main函数,是程序执行的入口
fmt.Println("Hello World!") // 在终端打印 Hello World!
}
go build
命令表示将源代码编译成可执行文件。
在hello目录下执行:
go build
或者在其他目录执行以下命令:
go build hello
go编译器会去 GOPATH
的src目录下查找你要编译的hello
项目
编译得到的可执行文件会保存在执行编译命令的当前目录下,如果是windows平台会在当前目录下找到hello.exe
可执行文件。
可在终端直接执行该hello.exe
文件:
c:\desktop\hello>hello.exe
Hello World!
我们还可以使用-o
参数来指定编译后得到的可执行文件的名字。
go build -o heiheihei.exe
go run main.go
也可以执行程序,该命令本质上也是先编译再执行。
go install
表示安装的意思,它先编译源代码得到可执行文件,然后将可执行文件移动到GOPATH
的bin目录下。因为我们的环境变量中配置了GOPATH
下的bin目录,所以我们就可以在任意地方直接执行可执行文件了。
最早的时候,Go所依赖的所有的第三方库都放在GOPATH这个目录下面。这就导致了同一个库只能保存一个版本的代码
godep
是一个通过vender模式实现的Go语言的第三方依赖管理工具,类似的还有由社区维护准官方包管理工具dep
。
Go语言从v1.5开始开始引入vendor
模式,如果项目目录下有vendor目录,那么go工具链会优先使用vendor
内的包进行编译、测试等。
自go1.11版本之后,go module将是go语言默认的依赖管理工具
使用go module管理依赖后辉仔项目根目录下生成两个文件go.mod
和go.sum
。
要启用go module
支持首先要设置环境变量GO111MODULE
,通过它可以开启或关闭模块支持,它有三个可选值:off
、on
、auto
,默认值是auto
。
GO111MODULE=off
禁用模块支持,编译时会从GOPATH
和vendor
文件夹中查找包。GO111MODULE=on
启用模块支持,编译时会忽略GOPATH
和vendor
文件夹,只根据 go.mod
下载依赖。GO111MODULE=auto
,当项目在$GOPATH/src
外且项目根目录有go.mod
文件时,开启模块支持。换句话说:开启GO111MODULE=on
后就不必在gopath中创建项目了,并且能够很好的管理项目依赖的第三方包信息。具有模块化支持
常用的go mod
命令如下:
go mod download 下载依赖的module到本地cache(默认为$GOPATH/pkg/mod目录)
go mod edit 编辑go.mod文件
go mod graph 打印模块依赖图
go mod init 初始化当前文件夹, 创建go.mod文件
go mod tidy 增加缺少的module,删除无用的module
go mod vendor 将依赖复制到vendor下
go mod verify 校验依赖
go mod why 解释为什么需要依赖
go.mod文件记录了项目所有的依赖信息,其结构大致如下:
module github.com/Q1mi/studygo/blogger
go 1.12
require (
github.com/DeanThompson/ginpprof v0.0.0-20190408063150-3be636683586
github.com/gin-gonic/gin v1.4.0
github.com/go-sql-driver/mysql v1.4.1
github.com/jmoiron/sqlx v1.2.0
github.com/satori/go.uuid v1.2.0
google.golang.org/appengine v1.6.1 // indirect
)
其中,
module
用来定义包名require
用来定义依赖包及版本indirect
表示间接引用关于依赖的版本支持以下几种格式:
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7
gopkg.in/vmihailenco/msgpack.v2 v2.9.1
gopkg.in/yaml.v2 <=v2.2.1
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e
latest
在国内访问golang.org/x的各个包都需要,你可以在go.mod中使用replace替换成github上对应的库。
replace (
golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac => github.com/golang/crypto v0.0.0-20180820150726-614d502a4dac
golang.org/x/net v0.0.0-20180821023952-922f4815f713 => github.com/golang/net v0.0.0-20180826012351-8a410e7b638d
golang.org/x/text v0.3.0 => github.com/golang/text v0.3.0
)
在项目中执行go get
命令可以下载依赖包,并且还可以指定下载的版本。
go get -u
将会升级到最新的次要版本或者修订版本(x.y.z, z是修订版本号, y是次要版本号)go get -u=patch
将会升级到最新的修订版本go get package@version
将会升级到指定的版本号version如果下载所有依赖可以使用go mod download
命令。
我们在代码中删除依赖代码后,相关的依赖库并不会在go.mod
文件中自动移除。这种情况下我们可以使用go mod tidy
命令更新go.mod
中的依赖关系。
格式化
go mod edit -fmt
添加依赖
go mod edit -require=golang.org/x/text
移除依赖
如果只是想修改go.mod
文件中的内容,那么可以运行go mod edit -droprequire=package path
,比如要在go.mod
中移除golang.org/x/text
包,可以使用如下命令:
go mod edit -droprequire=golang.org/x/text
关于go mod edit
的更多用法可以通过go help mod edit
查看。
如果需要对一个已经存在的项目启用go module
,可以按照以下步骤操作:
go mod init
,生成一个go.mod
文件。go get
,查找并记录当前项目的依赖,同时生成一个go.sum
记录每个依赖库的版本和哈希值。对于一个新创建的项目,我们可以在项目文件夹下按照以下步骤操作:
go mod init 项目名
命令,在当前项目文件夹下创建一个go.mod
文件。go.mod
中的require依赖项或执行go get
自动发现、维护依赖。其中,
module
用来定义包名require
用来定义依赖包及版本indirect
表示间接引用replace
代替跳墙go module
是Go1.11版本之后官方推出的版本管理工具,并且从Go1.13
版本开始,go module
将是Go语言默认的依赖管理工具。到今天Go1.14
版本推出之后Go modules
功能已经被正式推荐在生产环境下使用了。
假设我们现在有文件目录结构如下:
└── bubble
├── dao
│ └── mysql.go
├── go.mod
└── main.go
其中bubble/go.mod
内容如下:
module github.com/q1mi/bubble
go 1.14
bubble/dao/mysql.go
内容如下:
package dao
import "fmt"
func New(){
fmt.Println("mypackage.New")
}
bubble/main.go
内容如下:
package main
import (
"fmt"
"github.com/q1mi/bubble/dao"
)
func main() {
dao.New()
fmt.Println("main")
}
我们现在有文件目录结构如下:
├── p1
│ ├── go.mod
│ └── main.go
└── p2
├── go.mod
└── p2.go
p1/main.go
中想要导入p2.go
中定义的函数。
p2/go.mod
内容如下:
module liwenzhou.com/q1mi/p2
go 1.14
p2/p2.go
内容如下:
package p2
import "fmt"
func New() {
fmt.Println("mypackage.New")
}
p1/main.go
中按如下方式导入
package main
import (
"fmt"
"liwenzhou.com/q1mi/p2"
)
func main() {
p2.New()
fmt.Println("main")
}
因为我并没有把liwenzhou.com/q1mi/p2
这个包上传到liwenzhou.com
这个网站,我们只是想导入本地的包,这个时候就需要用到replace
这个指令了。
p1/go.mod
内容如下:
module github.com/q1mi/p1
go 1.14
require "liwenzhou.com/q1mi/p2" v0.0.0
replace "liwenzhou.com/q1mi/p2" => "../p2"
Go语言中标识符由字母数字和_
(下划线)组成,并且只能以字母和_
开头。
25个关键字,37个保留字
关键字是指编程语言中预先定义好的具有特殊含义的标识符。 关键字和保留字都不建议用作变量名。
Go语言中有25个关键字:
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
此外,Go语言中还有37个保留字。
Constants: true false iota nil
Types: int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64 complex128 complex64
bool byte rune string error
Functions: make len cap new append copy close delete
complex real imag
panic recover
由于程序运行过程中数据都是保存在内存中,如果我们在代码中通过内存地址去操作变量,代码的可读性非常差而且容易出错,所以产生了变量,利用变量将这个数据内存地址保存起来,以后直接通过这个变量就能找到内存上对应的数据
变量需要声明后才能使用,同一作用域内不支持重复声明。 并且Go语言的变量声明后必须使用。
声明格式:
var 变量名 变量类型
批量声明
var (
a String
b int
c bool
d float32
)
var 变量名 类型 = 表达式
举例:一次初始化多个变量
var name,age = "Q1mi",20
有时候我们会将变量的类型省略,这个时候编译器会根据等号右边的值来推导变量的类型完成初始化。
var name = "Q1mi"
var age = 18
在函数内部,可以使用更简略的 :=
方式声明并初始化变量。
package main
import (
"fmt"
)
// 全局变量m
var m = 100
func main() {
n := 10
m := 200 // 此处声明局部变量m
fmt.Println(m, n)
}
在多重赋值时如果要忽略某个值,可以使用匿名变量,用_表示
如:
func foo() (int,string) {
return 10,"Q1mi"
}
func main() {
x,_ := foo()
_,y := foo()
fmt.Println("x=",x)
fmt.Println("y=",y)
}
匿名变量不占用内存空间,所以也不存在重复声明
注意事项:
0
空字符串
false
nil
对于变量来说,常量是恒定不变的值
多个常量也可以一起声明
const (
pi = 3.1415
e = 2.718
)
const同时声明多个常量时,如果省略了值则表示和上面一行的值相同
const (
n1 = 100
n2
n3
)
//n1=n2=n3=100
go语言的常量计数器,只能在常量的表达式中使用
iota
计数一次(iota可理解为const语句块中的行索引)。const (
n1 = iota //0
n2 //1
n3 //2
n4 //3
)
_
跳过某些值const (
n1 = iota //0
n2 //1
_
n3 //3
)
iota
声明中间插队const (
n1 = iota //0
n2 = 100 //100
n3 = iota //2
n4 //3
)
const n5 = iota //0
iota
定义在一行const (
a,b = iota+1 , iota+2 //1,2
c,d //2,3
e,f //3,4
)
定义数量级 (这里的<<
表示左移操作,1<<10
表示将1的二进制表示向左移10位,也就是由1
变成了10000000000
,也就是十进制的1024。同理2<<2
表示将2的二进制表示向左移2位,也就是由10
变成了1000
,也就是十进制的8。)
const (
_ = iota
KB = 1 << (10 * iota)
MB = 1 << (10 * iota)
GB = 1 << (10 * iota)
TB = 1 << (10 * iota)
PB = 1 << (10 * iota)
)
基本数据类型(7个)
引用数据类型(6个)
复合数据类型
啥
两大类
其中,uint8
就是我们熟知的byte
型,int16
对应C语言中的short
型,int64
对应C语言中的long
型。
类型 | 描述 |
---|---|
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) |
特殊整型
类型 | 描述 |
---|---|
uint | 32位操作系统上就是uint32 ,64位操作系统上就是uint64 |
int | 32位操作系统上就是int32 ,64位操作系统上就是int64 |
uintptr | 无符号整型,用于存放一个指针 |
注意事项 获取对象的长度的内建len()
函数返回的长度可以根据不同平台的字节长度进行变化。实际使用中,切片或 map 的元素数量等都可以用int
来表示。在涉及到二进制传输、读写文件的结构描述时,为了保持文件的结构不会受到不同编译目标平台字节长度的影响,不要使用int
和 uint
。
数字字面量语法
_
来分隔数字,比如说: v := 123_456
表示 v 的值等于 123456。==Go语言支持两种浮点型数:float32
和float64
。==这两种浮点型数据格式遵循IEEE 754
标准: float32
的浮点数的最大范围约为 3.4e38
,可以使用常量定义:math.MaxFloat32
。 float64
的浮点数的最大范围约为 1.8e308
,可以使用一个常量定义:math.MaxFloat64
。
打印浮点数时,可以使用fmt
包配合动词%f
,代码如下:
package main
import (
"fmt"
"math"
)
func main() {
var a = math.MaxFloat32
fmt.Printf("%f\n", math.Pi)
fmt.Printf("%.2f\n", math.Pi)
fmt.Println(a)
}
complex64和complex128
var c1 complex64
c1 = 1 + 2i
var c2 complex128
c2 = 2 + 3i
fmt.Println(c1)
fmt.Println(c2)
复数有实部和虚部,complex64的实部和虚部为32位,complex128的实部和虚部为64位。
Go语言中以bool
类型进行声明布尔型数据,布尔型数据只有true(真)
和false(假)
两个值。
注意:
false
。转义符 | 含义 |
---|---|
\r |
回车符(返回行首) |
\n |
换行符(直接跳到下一行的同列位置) |
\t |
制表符 |
\' |
单引号 |
\" |
双引号 |
\\ |
反斜杠 |
不许使用反引号
字符,如
s1 := `第一行
第二行
第三行
`
fmt.Println(s1)
方法 | 介绍 |
---|---|
len(str) | 求长度 |
+或fmt.Sprintf | 拼接字符串 |
strings.Split | 分割 |
strings.contains | 判断是否包含 |
strings.HasPrefix,strings.HasSuffix | 前缀/后缀判断 |
strings.Index(),strings.LastIndex() | 子串出现的位置 |
strings.Join(a[]string, sep string) | join操作 |
字符用单引号(’)包裹起来
var a = '中'
var b = 'x'
Go 语言的字符有以下两种:
uint8
类型,或者叫 byte 型,代表了ASCII码
的一个字符。rune
类型,代表一个 UTF-8字符
。当需要处理中文、日文或者其他复合字符时,则需要用到rune
类型。rune
类型实际是一个int32
。
修改字符串
要修改字符串,需要先将其转换成[]rune
或[]byte
,完成后再转换为string
。无论哪种转换,都会重新分配内存,并复制字节数组。
package main
import "fmt"
func main() {
traversalString()
changeString()
}
//遍历字符串
func traversalString() {
s := "hello沙河"
for i := 0; i < len(s); i++ {
fmt.Printf("%v(%c)", s[i], s[i])
}
fmt.Println()
for _, r := range s {
fmt.Printf("%v(%c)", r, r)
}
fmt.Println()
}
//修改字符串
func changeString() {
s1 := "big"
//强制类型转换
byteS1 := []byte(s1)
byteS1[0] = 'p'
fmt.Println(string(byteS1))
s2 := "白萝卜"
runeS2 := []rune(s2)
runeS2[0] = '红'
fmt.Println(string(runeS2))
}
Go语言中只有强制类型转换,没有隐式类型转换。该语法只能在两个类型之间支持相互转换的时候使用。
强制类型转换的基本语法如下:
T(表达式)
其中,T表示要转换的类型。表达式包括变量、复杂算子和函数返回值等
编写代码统计出字符串"hello沙河小王子"
中汉字的数量。
//统计编写代码统计出字符串"hello沙河小王子"中汉字的数量。
func num() {
s := "hello沙河小王子"
a := 0
for _, r := range s {
if r > 'z' {
a++
}
}
fmt.Println(a)
}
Go 语言内置的运算符有:
https://www.liwenzhou.com/posts/Go/03_operators/
Go语言中最常用的流程控制有if
和for
,而switch
和goto
主要是为了简化代码、降低重复代码而生的结构,属于扩展类的流程控制。
Go语言基础之流程控制 | 李文周的博客 (liwenzhou.com)
if 表达式1 {
分支1
} else if 表达式2 {
分支2
} else{
分支3
}
if条件判断还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断,举个例子:
func ifDemo2() {
if score := 65; score >= 90 {
fmt.Println("A")
} else if score > 75 {
fmt.Println("B")
} else {
fmt.Println("C")
}
}
for循环的基本格式如下:
for 初始语句;条件表达式;结束语句{
循环体语句
}
for循环的初始语句可以被忽略,但是初始语句后的分号必须要写,例如:
func forDemo2() {
i := 0
for ; i < 10; i++ {
fmt.Println(i)
}
}
for循环的初始语句和结束语句都可以省略,例如:
func forDemo3() {
i := 0
for i < 10 {
fmt.Println(i)
i++
}
}
这种写法类似于其他编程语言中的while
,在while
后添加一个条件表达式,满足条件表达式时持续循环,否则结束循环。
for {
循环体语句
}
for循环可以通过break
、goto
、return
、panic
语句强制退出循环。
Go语言中可以使用for range
遍历数组、切片、字符串、map 及通道(channel)。 通过for range
遍历的返回值有以下规律:
Go语言规定每个switch
只能有一个default
分支。.
func switchDemo1() {
finger := 3
switch finger {
case 1:
fmt.Println("大拇指")
case 2:
fmt.Println("食指")
case 3:
fmt.Println("中指")
default:
fmt.Println("无效的输入")
}
}
一个分支可以有多个值,多个case值中间使用英文逗号分隔。
func testSwitch3() {
switch n:=7;n {
case 1,3,6,8,9:
fmt.Println("奇数")
case 2,4,6,8:
fmt.Println("偶数")
default:
fmt.Println(n)
}
}
分支还可以使用表达式,这时候switch语句后面不需要再跟判断变量。例如:
func switchDemo4() {
age := 30
switch {
case age<25:
fmt.Println("好好学习吧")
case age>25 && age<35:
fmt.Println("好好吃饭吧")
case age>60:
fmt.Println("好好干活吧")
default:
fmt.Println("好好活着")
}
}
fallthrough
语法可以执行满足条件的case的下一个case,是为了兼容C语言中的case设计的。
func switchDemo5() {
s := "a"
switch {
case s == "a":
fmt.Println("a")
fallthrough
case s == "b":
fmt.Println("b")
case s == "c":
fmt.Println("c")
default:
fmt.Println("...")
}
}
输出:
a
b
goto
语句通过标签进行代码间的无条件跳转。goto
语句可以在快速跳出循环、避免重复退出上有一定的帮助。Go语言中使用goto
语句能简化一些代码的实现过程。 例如双层嵌套的for循环要退出时:
//不使用goto
func gotoDemo1() {
var breakFlag bool
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if j == 2 {
breakFlag = true
break
}
fmt.Printf("%v-%v\n", i, j)
}
if breakFlag {
break
}
}
}
//使用goto简化代码
func gotoDemo2() {
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if j == 2 {
goto breakTag
}
fmt.Printf("%v-%v\n", i, j)
}
}
return
breakTag:
fmt.Println("结束for循环")
}
break
语句还可以在语句后面添加标签,表示退出某个标签对应的代码块,标签要求必须定义在对应的for
、switch
和 select
的代码块上。 举个例子:
func breakDemo1() {
BREAKDEMO1:
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if j == 2 {
break BREAKDEMO1
}
fmt.Printf("%v-%v\n", i, j)
}
}
fmt.Println("...")
}
continue
语句可以结束当前循环,开始下一次的循环迭代过程,仅限在for
循环内使用。
在 continue
语句后添加标签时,表示开始标签对应的循环。例如:
func continueDemo() {
forloop1:
for i := 0; i < 5; i++ {
// forloop2:
for j := 0; j < 5; j++ {
if i == 2 && j == 2 {
continue forloop1
}
fmt.Printf("%v-%v\n", i, j)
}
}
}
var 数组变量名 [元素数量]T
初始化数组时可以使用初始化列表来设置数组元素的值。
func main() {
var testArray [3]int //数组会初始化为int类型的零值
var numArray = [3]int{1, 2} //使用指定的初始值完成初始化
var cityArray = [3]string{"北京", "上海", "深圳"} //使用指定的初始值完成初始化
fmt.Println(testArray) //[0 0 0]
fmt.Println(numArray) //[1 2 0]
fmt.Println(cityArray) //[北京 上海 深圳]
}
上面那种方法每次都要给定一个确定的长度,下面我们让编译器自行推断数组的长度
func main() {
var testArray [3]int
var numArray = [...]int{1, 2}
var cityArray = [...]string{"北京", "上海", "深圳"}
fmt.Println(testArray) //[0 0 0]
fmt.Println(numArray) //[1 2]
fmt.Printf("type of numArray:%T\n", numArray) //type of numArray:[2]int
fmt.Println(cityArray) //[北京 上海 深圳]
fmt.Printf("type of cityArray:%T\n", cityArray) //type of cityArray:[3]string
}
使用指定索引值的方式来初始化数组
func main() {
a := [...]int{1: 1, 3: 5}
fmt.Println(a) // [0 1 0 5]
fmt.Printf("type of a:%T\n", a) //type of a:[4]int
}
func main() {
var a = [...]string{"北京", "上海", "深圳"}
//方法一遍历
for i := 0; i < len(a); i++ {
fmt.Println(a[i])
}
//方法二 for range遍历
for index, value := range a {
fmt.Println(index, value)
}
}
func main() {
a := [3][2]string{
{"北京", "上海"},
{"广州", "深圳"},
{"成都", "重庆"},
}
for _, v1 := range a {
for _, v2 := range v1 {
fmt.Printf("%s\t", v2)
}
fmt.Println()
}
}
注意: 多维数组只有第一层可以使用...
来让编译器推导数组长度。例如:
//支持的写法
a := [...][2]string{
{"北京", "上海"},
{"广州", "深圳"},
{"成都", "重庆"},
}
数组是值类型,赋值和传参会复制整个数组。因此改变副本的值,不会改变本身的值。
func main() {
a := [3]int{10, 20, 30}
modifyArray(a)
fmt.Println(a)
b := [3][2]int{
{1, 1},
{1, 1},
{1, 1},
}
modifyArray2(b)
fmt.Println(b)
}
func modifyArray(x [3]int) {
x[0] = 100
}
func modifyArray2(x [3][2]int) {
x[2][0] = 100
}
输出:
[10 20 30]
[[1 1] [1 1] [1 1]]
注意:
[n]*T
表示指针数组,*[n]T
表示数组指针 。切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。支持自动扩容。
切片是一个引用类型,它的内部结构包含地址
、长度
和容量
。切片一般用于快速地操作一块数据集合。
声明切片类型的基本语法如下:
var name []T
func main() {
// 声明切片类型
var a []string //声明一个字符串切片
var b = []int{} //声明一个整型切片并初始化
var c = []bool{false, true} //声明一个布尔切片并初始化
var d = []bool{false, true} //声明一个布尔切片并初始化
fmt.Println(a) //[]
fmt.Println(b) //[]
fmt.Println(c) //[false true]
fmt.Println(a == nil) //true
fmt.Println(b == nil) //false
fmt.Println(c == nil) //false
// fmt.Println(c == d) //切片是引用类型,不支持直接比较,只能和nil比较
}
切片拥有自己的长度,可以通过内置的len()函数求长度,使用内置的cap()函数求切片的容量
简单切片表达式
low
和high
表示一个索引范围(左包含,右不包含)1<=索引值<4
的元素组成切片s,得到的切片长度=high-low
,func main() {
a := [5]int{1, 2, 3, 4, 5}
s := a[1:3] // s := a[low:high]
fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s))
}
输出:
s:[2 3] len(s):2 cap(s):4
对于数组或字符串,如果0 <= low <= high <= len(a)
,则索引合法,否则就会索引越界(out of range)。
完整切片表达式
对于数组,指向数组的指针,或切片a(注意不能是字符串)支持完整切片表达式:
a[low : high : max]
a[low: high]
相同类型、相同长度和元素的切片。max-low
。完整切片表达式需要满足的条件是0 <= low <= high <= max <= cap(a)
,其他条件和简单切片表达式相同。
如果不基于数组创建切片,我们需要动态创建一个切片,我们需要使用内置的make()函数
make([]T, size, cap)
切片的本质就是对底层数组的封装,它包含了三个信息:
现在有一个数组a := [8]int{0, 1, 2, 3, 4, 5, 6, 7}
,切片s1 := a[:5]
,相应示意图如下。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E6P6ZIdF-1655688771942)(go基础.assets/image-20220617203649168.png)]
切片s2 := a[3:6]
,相应示意图如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jx8xMJB4-1655688771943)(go基础.assets/image-20220617203711239.png)]
要检查切片是否为空,请始终使用len(s) == 0
来判断,而不应该使用s == nil
来判断。
==
来比较两个切片是否含有全部相等的元素var s1 []int //len(s1)=0;cap(s1)=0;s1==nil
s2 := []int{} //len(s2)=0;cap(s2)=0;s2!=nil
s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil
所以要判断一个切片是否为空是用len(s)==0来判断,不能使用s ==nil
切片拷贝后生成的新的切片和原来的切片共享同一个底层数组,对一个切片的修改会影响另一个切片的内容
func main() {
s1 := make([]int, 3) //[0 0 0]
s2 := s1 //将s1直接赋值给s2,s1和s2共用一个底层数组
s2[0] = 100
fmt.Println(s1) //[100 0 0]
fmt.Println(s2) //[100 0 0]
}
可以使用copy() 函数复制切片 解决这一问题
和数组一致,支持索引遍历和for range遍历
func main() {
s := []int{1, 3, 5}
for i := 0; i < len(s); i++ {
fmt.Println(i, s[i])
}
fmt.Println("=======")
for index, value := range s {
fmt.Println(index, value)
}
}
Go语言的内建函数append()
可以为切片动态添加元素。 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)。
func main(){
var s []int
s = append(s, 1) // [1]
s = append(s, 2, 3, 4) // [1 2 3 4]
s2 := []int{5, 6, 7}
s = append(s, s2...) // [1 2 3 4 5 6 7]
}
注意:通过var声明的零值切片可以在append()
函数直接使用,无需初始化。
每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()
函数调用时,所以我们通常都需要用原变量接收append函数的返回值。
通过查看$GOROOT/src/runtime/slice.go
源码,其中扩容相关代码如下:
//新申请的容量 cap
//旧容量 old.cap
//最终容量 newcap
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
从上面的代码可以看出以下内容:
需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如int
和string
类型的处理方式就不一样。
由于切片是引用类型,所以a和b其实都指向了同一块内存地址。修改b的同时a的值也会发生变化。
Go语言内建的copy()
函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()
函数的使用格式如下:
copy(destSlice, srcSlice []T)
其中:
func main() {
// copy()复制切片
a := []int{1, 2, 3, 4, 5}
c := make([]int, 5, 5)
copy(c, a) //使用copy()函数将切片a中的元素复制到切片c
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1 2 3 4 5]
c[0] = 1000
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1000 2 3 4 5]
}
go语言中没有删除元素专用的方法,我们可以用切片本身的性质来删除元素
func main() {
//从切片中删除元素
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
//删除索引为2的元素
a = append(a[:2],a[3:]...)
fmt.Println(a) //[30 31 33 34 35 36 37]
}
总结一下就是:要从切片a中删除索引为index
的元素,操作方法是a = append(a[:index], a[index+1:]...)
Go语言中提供的映射关系容器为map
,其内部使用散列表(hash)
实现。
Go语言中 map
的定义语法如下:
map[KeyType]ValueType
其中
map类型的变量默认值为nil,需要使用make()函数来分配内存,语法为
make(map[KeyType]ValueType,[cap]) //cap表示map的容量
其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。
func main() {
scoreMap := make(map[string]int, 8)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
fmt.Println(scoreMap)
fmt.Println(scoreMap["小明"])
fmt.Printf("type of a:%T\n", scoreMap)
}
map也支持在声明的时候填充元素,例如:
func main() {
userInfo := map[string]string{
"username": "沙河小王子",
"password": "123456",
}
fmt.Println(userInfo) //
}
Go语言中有个判断map中键是否存在的特殊写法,格式如下:
value,ok :=map[key]
举个例子:
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
// 如果key存在ok为true,v为对应的值;不存在ok为false,v为值类型的零值
v, ok := scoreMap["张三"]
if ok {
fmt.Println(v)
} else {
fmt.Println("查无此人")
}
}
go语言中使用for range遍历map
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["娜扎"] = 60
for key,value := range scoreMap {
fmt.println(key,value)
}
}
只想遍历key的时候按照下面的写法
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["娜扎"] = 60
for key := range scoreMap{
fmt.Println(k)
}
}
注意: 遍历map时的元素顺序与添加键值对的顺序无关。
使用delete()内建函数从map中删除一组键值对,delete()函数格式如下
delete(map,key)
其中,
如:
func main() {
scoreMap := map[string]int{
"张三":90,
"小明":100,
"娜扎":60,
}
delete(scoreMap,"娜扎")
for k,v := range scoreMap{
fmt.Println(k,v)
}
}
不加随机种子,每次遍历获取都是重复的一些随机数据
func main() {
rand.Seed(time.Now().UnixNano())//初始化随机数种子,不加随机种子,每次遍历获取都是重复的一些随机数据
var scoreMap = make(map[string]int,200)
for i:=0;i<100;i++ {
key := fmt.Sprintf("stu%02d",i)
value := rand.Intn(100)
scoreMap[key] = value
}
//取出map中所有的key,存入切片keys中
var keys = make([]string,0,200)
for key := range scoreMap {
keys = append(keys,key)
}
//对切片进行排序
for _,key := range keys {
fmt.Println(key,scoreMap[key])
}
}
func main() {
var mapSlice = make([]map[string]string,3)
for index,value := range mapSlice {
fmt.Printf("index:%d value:%v\n",index,value)
}
fmt.Println("after init")
//对切片中的元素进行初始化
mapSlice[0] = make(map[string]string,10)
mapSlice[0]["name"] = "小王子"
mapSlice[0]["password"] = "123456"
mapSlice[0]["address"] = "沙河"
for index,value := range mapSlice {
fmt.Printf("index:%d value:%v\n",index,value)
}
}
输出:
index:0 value:map[]
index:1 value:map[]
index:2 value:map[]
after init
index:0 value:map[address:沙河 name:小王子 password:123456]
index:1 value:map[]
index:2 value:map[]
//二维数组
func main() {
var sliceMap = make(map[string][]string, 3)
fmt.Println(sliceMap)
fmt.Println("after init")
key := "中国"
value, ok := sliceMap[key]
if !ok {
value = make([]string, 0, 2)
}
value = append(value, "北京", "上海")
sliceMap[key] = value
fmt.Println(sliceMap)
}
定义函数使用func
关键字,格式如下
func 函数名(参数) (返回值) {
函数体
}
其中,
,
分隔。()
包裹,并用,
分隔。定义函数使用func
关键字,格式如下
func 函数名(参数) (返回值) {
函数体
}
其中,
,
分隔。()
包裹,并用,
分隔。例如两数求和:
func intSum(x int,y int) int{
return x+y
}
既不需要参数也不需要返回值:
func sayHello() {
fmt.Println("Hello 沙河")
}
定义了函数之后,我们可以通过函数名()
的方式调用函数。例如调用上面两个函数代码如下
func main() {
sayHello()
ret := intSum(10,20)
fmt.Println(ret)
}
注意,调用有返回值的函数时,可以不接收其返回值。
参数中如果相邻变量的类型相同,则可以省略参数
func intSum(x,y int) int {
return x+y
}
可变参数是指函数的参数数量是不固定的,go语言中可变参数通过在参数名后加···
来标识
注意:可变参数通常要做为函数的最后一个参数
func intSum2(x ...int) int {
fmt.Println(x)
sum := 0
for _,v := range x {
sum = sum+v
}
return sum
}
调用上面的函数:
ret1 := intSum2()
ret2 := intSum2(10)
ret3 := intSum2(10, 20)
ret4 := intSum2(10, 20, 30)
fmt.Println(ret1, ret2, ret3, ret4) //0 10 30 60
固定参数搭配可变参数使用时,可变参数要放在固定参数的后面
func intSum3(x int,y ...int) int {
fmt.Println(x,y)
sum := x
for _,v :=range y {
sum = sum+v
}
return sum
}
调用上述函数:
ret5 := intSum3(100)
ret6 := intSum3(100, 10)
ret7 := intSum3(100, 10, 20)
ret8 := intSum3(100, 10, 20, 30)
fmt.Println(ret5, ret6, ret7, ret8) //100 110 130 160
本质上,函数的可变参数是通过切片来实现的。
go语言中函数支持多返回值,函数如果有多个返回值时必须用( )将所有返回值包裹起来 (int,int)
func calc(x,y int) (int,int) {
sum := x+y
sub := x-y
return sum,sub
}
函数定义时可以给返回值命名,在函数体中使用这些变量,最后通过return关键字返回
func calc(x,y int) (sum,sub int) {
sum = x+y
sub = x-y
return
}
当我们的返回值类型为slice时,nil可以看作一个有效的spice,没有必要显示返回一个长度为0的切片。
func someFunc(x string) []int {
if x=="" {
return nil //没有必要返回[]int {}
}
}
全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。 在函数中可以访问到全局变量。
局部变量又分为两种:
type
关键字定义一个函数类型,具体格式如下:
type calculation func(int,int) int
上面语句定义了一个calculation
类型,它是一种函数类型,这种函数接收两个int类型的参数并且返回一个int类型的返回值。
简单来说,凡是满足这个条件的函数都是calculation类型的函数,例如下面的add和sub是calculation类型。
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}
add和sub都能赋值给calculation类型的变量
var c calculation
c = add
我们可以声明函数类型的变量并未该变量赋值
func main() {
type calculation func(int, int) int
var c calculation
c = add
fmt.Printf("type of c:%T\n", c)
fmt.Println(1, 2)
f := add
fmt.Printf("type of f:%T\n", f)
fmt.Println(f(10, 20))
}
func add(x, y int) int {
return x + y
}
func main() {
var c calculation // 声明一个calculation类型的变量c
c = add // 把add赋值给c
fmt.Printf("type of c:%T\n", c) // type of c:main.calculation
fmt.Println(c(1, 2)) // 像调用add一样调用c
f := add // 将函数add赋值给变量f1
fmt.Printf("type of f:%T\n", f) // type of f:func(int, int) int
fmt.Println(f(10, 20)) // 像调用add一样调用f
}
高阶函数分为函数作为参数和函数作为返回值两部分
func add(x,y int) int {
return x+y
}
func calc(x,y int,op func(int,int)int) int {
return op(x,y)
}
func main() {
ret2 := calc(10,20,add)
fmt.Println(ret2) //30
}
func do(s string) (func(int,int)int,error) {
switch s {
case "+":
return add,nil
case "-":
return sub,nil
default:
err := errors.New("无法识别的操作符")
return nil,err
}
}
匿名函数就是没有函数名的函数,格式如下
func (参数)(返回值) {
函数值
}
匿名函数由于没有函数名,所以无法像普通函数那样调用。所以匿名函数要保存到某个变量或作为立即执行函数:
func main() {
//将匿名函数保存到变量
add := func(x,y int) {
fmt.Println(x+y)
}
add(10,20)
//自执行函数:匿名函数定义完加()直接执行
func(x,y int) {
fmt.Println(x+y)
}(10,30)
}
匿名函数多用于实现回调函数和闭包。
闭包指的是一个函数和与其相关的引用环境组合而成的实体。换句话:闭包=函数+引用环境
。例如
func adder() func(int) int {
var x int
return func(y int) int {
x += y
return x
}
}
func main() {
var f = adder()
fmt.Println(f(10)) //10
fmt.Println(f(20)) //30
fmt.Println(f(30)) //60
f1 := adder()
fmt.Println(f1(40)) //40
fmt.Println(f1(50)) //90
}
变量f
是一个函数并且它引用了其外部作用域中的x
变量,此时f
就是一个闭包。 在f
的生命周期内,变量x
也一直有效。 闭包进阶示例1:
func adder2(x int) func(int) int {
return func(y int) int {
x += y
return x
}
}
func main() {
var f = adder2(10)
fmt.Println(f(10)) //20
fmt.Println(f(20)) //40
fmt.Println(f(30)) //70
f1 := adder2(20)
fmt.Println(f1(40)) //60
fmt.Println(f1(50)) //110
}
闭包进阶示例2:
func makeSuffixFunc(suffix string) func(string) string {
fmt.Println(suffix) //.jpg .txt
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")) //test.jpg
fmt.Println(txtFunc("test")) //test.txt
}
闭包进阶示例3:(不断累加)
func calc(base int) (func(int) int, func(int) int) {
add := func(i int) int {
base += i
return base
}
sub := func(i int) int {
base -= i
return base
}
return add, sub
}
func main() {
f1, f2 := calc(10)
fmt.Println(f1(1), f2(2)) //11 9
fmt.Println(f1(3), f2(4)) //12 8
fmt.Println(f1(5), f2(6)) //13 7
}
defer
语句会将其后面跟随的语句进行延迟处理。defer
归属的函数即将返回时,将延迟处理的语句按defer
定义的逆序进行执行defer
的语句最后被执行,最后被defer
的语句,最先被执行。举例:
func main() {
fmt.Println("start")
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
fmt.Println("end")
}
输出:
start
end
3
2
1
由于defer
语句延迟调用的特性,所以defer
语句能非常方便的处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等。
在Go语言的函数中return
语句在底层并不是原子操作,它分为
而defer
语句执行的时机就在返回值赋值操作后,RET指令执行前。具体如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LslSekP9-1656895196619)(go基础.assets/image-20220620162920004.png)]
Go语言基础之函数 | 李文周的博客 (liwenzhou.com)
没看懂
func f1() int {
x := 5
defer func() {
x++
}()
return x
}
func f2() (x int) {
defer func() {
x++
}()
return 5
}
func f3() (y int) {
x := 5
defer func() {
x++
}()
return x
}
func f4() (x int) {
defer func(x int) {
x++
}(x)
return 5
}
func main() {
fmt.Println(f1()) //5
fmt.Println(f2()) //6
fmt.Println(f3()) //5
fmt.Println(f4()) //5
}
内置函数 | 介绍 |
---|---|
close | 主要用来关闭channel |
len | 用来求长度,比如string、array、slice、map、channel |
new | 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针 |
make | 用来分配内存,主要用来分配引用类型,比如chan、map、slice |
append | 用来追加元素到数组、slice中 |
panic和recover | 用来做错误处理 |
Go语言中目前(Go1.12)是没有异常机制,但是使用panic/recover
模式来处理错误。 panic
可以在任何地方引发,但recover
只有在defer
调用的函数中有效。 首先来看一个例子:
func funcA() {
fmt.Println("func A")
}
func funcB() {
panic("panic in B")
}
func funcC() {
fmt.Println("func C")
}
func main() {
funcA()
funcB()
funcC()
}
输出:
func A
panic: panic in B
goroutine 1 [running]:
main.funcB(...)
.../code/func/main.go:12
main.main()
.../code/func/main.go:20 +0x98
程序运行期间funcB
中引发了panic
导致程序崩溃,异常退出了。这个时候我们就可以通过recover
将程序恢复回来,继续往后执行。
func funcA() {
fmt.Println("func A")
}
func funcB() {
defer func() {
err := recover()
//如果程序出出现了panic错误,可以通过recover恢复过来
if err != nil {
fmt.Println("recover in B")
}
}()
panic("panic in B")
}
func funcC() {
fmt.Println("func C")
}
func main() {
funcA()
funcB()
funcC()
}
注意:
recover()
必须搭配defer
使用。defer
一定要在可能引发panic
的语句之前定义。数据在内存中的地址就是指针
&
(取地址)和*
(根据地址取值)。*int
、*int64
、*string
等。取变量指针的语法格式
ptr := &v /v的类型为T
其中,
*T
,称做T的指针类型。*代表指针举例:
func main() {
a := 10
b := &a
fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078
fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int
fmt.Println(&b) // 0xc00000e018
}
b := &a
的图示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wm1LS6Pi-1656895196622)(go基础.assets/image-20220621143704827.png)]
在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针 *操作,也就是指针取值
指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值
func main() {
//指针取值
a := 10
b := &a //取变量a的地址,将指针保存到b中
fmt.Printf("type of b:%T\n",b)
c:=*b //指针取值,根据内存地址取值===>取到a的值10
fmt.Printf("type of c:%T\n",c)
fmt.Printf("value of c:%v\n",c)
}
输出如下:
type of b:*int
type of c:int
value of c:10
**总结:*取地址操作符&和取值操作符 是一对互补的操作,&取出地址,*根据地址取出地址指向的值
变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:
指针传值示列:
没懂
func modify1(x int) {
x = 100
}
func modify2(x *int) {
*x = 100
}
func main() {
a := 10
modify1(a)
fmt.Println(a) //10
modify2(&a)
fmt.Println(a) //100
}
//引用类型的没有分配内存,报异常 panic
func main() {
var a *int
//a = new(int)
*a = 100
fmt.Println(*a)
var b map[string]int
//b = make(map[string]int, 10)
b["沙河娜扎"] = 100
fmt.Println(b)
}
执行上面的代码会引发panic?
new是一个内置的函数,函数标签名如下:
func new (Type) *Type
其中,
new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值,举例:
func main () {
//a b属于指针类型
a := new(int)
b := new(bool)
fmt.Printf("%T\n",a) //*int
fmt.Printf("%T\n",b) //*bool
fmt.Println(*a) //0
fmt.Println(*b) // false
}
开始的示例代码中var a *int
只是声明了一个指针变量a但是没有初始化,指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值。应该按照如下方式使用内置的new函数对a进行初始化之后就可以正常对其赋值了:
func main() {
var a *int
a = new(int)
*a = 10
fmt.Println(*a)
}
func make(t Type,size ...IntegerType) Type
make函数是无可替代的,我们使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对他们进行操作
开头示例:var b map[string]int
只是声明变量b是一个map类型的变量,需要像下面的示例代码一样使用make函数进行初始化操作之后,才能对其进行键值对赋值:
func main() {
var b map[string]int
b = make(map[string]int, 10)
b["沙河娜扎"] = 100
fmt.Println(b)
}
string
、整型
、浮点型
、布尔
等数据类型type
关键字来定义自定义类型。自定义类型是定义了一个全新的类型,我们可以基于内置的基本类型定义,也可以通过struct定义。例如:
//将MyInt 定义为int类型
type MyInt int
通过type关键字定义,MyInt就是一种新的类型,它具有int的特性
类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias于Type是同一个类型。
type TypeAlias = type
我们之前见过的rune
和byte
就是类型别名,他们的定义如下:
type byte = uint8
type rune = int32
类型别名与类型定义表面上看只有一个等号的差异,一个是定义了新的类型,一个是给原来类型起个名字。下面代码来区别
//类型定义
type Newint int
//类型别名
type MyInt = int
func main () {
var a NewInt
var b MyInt
fmt.Printf("type of a:%T\n",a) //type of a:main.NewInt
fmt.Printf("type of b:%T\n",b) //type of b:int
}
结果显示
main.NewInt
,表示main包下定义的NewInt
类型。int
。MyInt
类型只会在代码中存在,编译完成时并不会有MyInt
类型。struct
来实现面向对象使用type
和struct
关键字来定义结构体。具体代码:
type 类型名 struct {
字段名 字段类型
字段名 字段类型
...
}
其中,
举例,定义一个Person
(人)结构体,代码:
type Person struct {
name string
city string
age int8
}
同样类型的字段也可以在写一行
type Person1 struct {
name,city string
age int8
}
这样我们使用这个person
结构体就能够很方便的在程序中表示和存储人信息了。
语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。
var 结构体实例 结构体类型
type Person struct {
name string
city string
age int8
}
func main() {
var p1 Person
p1.name = "沙河娜扎"
p1.city = "北京"
p1.age = 18
fmt.Printf("p1=%v\n",p1) //p1={沙河娜扎 北京 18}
fmt.Printf("p1=%#v\n",p1) //p1=main.person{name:"沙河娜扎", city:"北京", age:18}
}
我们通过.
来访问结构体的字段(成员变量),例如p1.name
和p1.age
等。
函数内部
func main() {
var user struct{Name string; Age int}
user.Name = "小王子"
user.Age = 18
fmt.Printf("%#v\n",user)
}
输出
struct { Name string; Age int }{Name:"小王子", Age:18}
我们还可以通过使用new关键字对结构体进行实例化,得到的是结构体地址,如下:
var p2 = new(person)
fmt.Printf("%T\n", p2) //*main.person
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"", city:"", age:0}
从打印的结果中我们可以看出p2
是一个结构体指针
需要注意的是在go语言中支持结构体指针直接使用.
来访问结构体成员
var p2 = new(person)
p2.name = "小王子"
p2.age = 28
p2.city = "上海"
fmt.Printf("p2=%#v\n",p2) //p2=&main.person{name:"小王子",city:"上海",age:28}
使用&
对结构体进行取地址操作相当于对该结构体类型进行了一次new
实例化操作
p3 := &person{}
fmt.Printf("%T\n", p3) //*main.person
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", city:"", age:0}
p3.name = "七米"
p3.age = 30
p3.city = "成都"
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"七米", city:"成都", age:30}
p3.name = "七米"
其实在底层是(*p3).name = "七米"
,这是Go语言帮我们实现的语法糖。
没有初始化的结构体,其成员变量都是对应其类型的零值
type person struct {
name string
city string
age int8
}
func main() {
var p4 person
fmt.Printf("p4=%#v\n", p4) //p4=main.person{name:"", city:"", age:0}
}
使用键值对对结构体进行初始化时,键对应的结构体字段,值对应该字段的初始值
p5 := person {
name:"小王子"
city:"北京"
age: 18,
}
fmt.Printf("p5=%#v\n",p5)
也可以对结构体指针进行键值对初始化,例如
p6 := &person {
name := "小艾王子"
city := "北京"
age :=18,
}
fmt.Printf("p6=%#v\n",p6)
当某些字段没有初始值的时候,该字段可以不写,此时,没有指定初始值的字段的值就是该字段类型的零值
p7 := &person {
city := "北京"
}
fmt.Printf("p7=%#v\n", p7) //p7=&main.person{name:"", city:"北京", age:0}
初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值:
p8 := &person{
"沙河娜扎",
"北京",
28,
}
fmt.Printf("p8=%#v\n",p8)
使用这种格式初始化时,需要注意:
结构体占用一块连续的内存
type test struct {
a int8
b int8
c int8
d int8
}
n := test {
1,2,3,4
}
fmt.Printf("n.a %p\n", &n.a)
fmt.Printf("n.b %p\n", &n.b)
fmt.Printf("n.c %p\n", &n.c)
fmt.Printf("n.d %p\n", &n.d)
输出:
n.a 0xc0000a0060
n.b 0xc0000a0061
n.c 0xc0000a0062
n.d 0xc0000a0063
空结构体是不占用空间的
var v struct{}
fmt.Println(unsafe.Sizeof(v)) //0
type student struct {
name string
age int
}
func main() {
m := make(map[string]*student)
stus := []student{
{name: "小王子", age: 18},
{name: "娜扎", age: 23},
{name: "大王八", age: 9000},
}
fmt.Println(stus)
for _, stu := range stus {
m[stu.name] = &stu
fmt.Println(stu)
}
fmt.Println(m)
for k, v := range m {
fmt.Println(k, "=>", v.name)
}
}
输出:
[{小王子 18} {娜扎 23} {大王八 9000}]
{小王子 18}
{娜扎 23}
{大王八 9000}
map[大王八:0xc0000040d8 娜扎:0xc0000040d8 小王子:0xc0000040d8]
娜扎 => 大王八
大王八 => 大王八
小王子 => 大王八
go语言的结构体没有构造函数,我们可以自己实现。例如,下面代码就实现了一个person构造函数。因为struct
是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型
func newPerson(name,city string,age int8) *person {
return &person {
name: name,
city: city,
age: age,
}
}
调用构造函数
p9 := newPerson("张三", "沙河", 90)
fmt.Printf("%#v\n", p9) //&main.person{name:"张三", city:"沙河", age:90}
go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)
。接收者的概念就类似于其他语言中的this或者self
方法定义格式如下:
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
其中,
self
、this
之类的命名。例如,Person
类型的接收者变量应该命名为 p
,Connector
类型的接收者变量应该命名为c
等。举例:
//Person 结构体
type Person struct {
name string
age int8
}
//NewPerson 构造函数
func NewPerson(name string,age int8) *Person{
return &Person {
name: name,
age: age,
}
}
//Dream Person做梦的方法
func (p Person) Dream() {
fmt.Printf("%s的梦想是学好go语言:\n",p.name)
}
func main() {
p1 := NewPerson("小王子",27)
p1.Dream()
}
没懂 函数和方法的区别:方法属于特定类型,函数不属于任何类型
指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后修改都是有效的。这种方式就十分接近于其他语言中的面向对象中的this或self。例如我们为Person添加一个SetAge方法,来修改实例变量的年龄。
//SetAge 设置p的年龄
//使用指针接收者
func (p *Person) SetAge(newAge int8) {
p.age = newAge
}
调用该方法
func main() {
p1 := NewPerson("小王子",25)
fmt.Println(p1.age) //25
p1.SetAge(30)
fmt.Println(p1.age) //30
}
当方法作用于值类型接收者时,go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。
//SetAge2 设置p的年龄
// 使用值接收者
func (p Person) setAge2(newAge int8) {
p.age = newAge
}
func main() {
p1 := NewPerson("小艾王子",25)
p1.Dream()
fmt.Println(p1.age) //25
p1.SetAge2(30) //(*p1).SetAge2(30)
fmt.Println(p1.age) //25
}
在go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法
//MyInt 将int定义为自定义的MyInt类型
type MyInt int
//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
fmt.Println("Hello,我是一个int")
}
func main() {
var m1 MyInt
m1.SayHello() //hello,我是一个int
m1 = 100
fmt.Printf("%#v %T\n",m1,m1) //100 main.MyInt
}
注意:非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就成为匿名字段
//Person 结构体Person类型
type Person struct {
string
int
}
func main() {
p1 := Person {
"小王子",
18,
}
fmt.Printf("%#v\n",p1) //main.Person{string:"北京", int:18}
fmt.Println(p1.string,p1.int) //北京 18
}
注意:这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
一个结构体中可以嵌套包含另一个结构体或结构体指针,例如:
//Adress 地址结构体
type Address struct {
Province string
City string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address Address
}
func main() {
user1 := User{
Name: "小王子",
Gender: "男",
Address: Address {
Province: "山东",
City: "威海",
},
}
fmt.Printf("user1=%#v\n", user1)
//user1=main.User{Name:"小王子", Gender:"男", Address:main.Address{Province:"山东", City:"威海"}}
}
上面user结构体中嵌套的Adress结构体也可同意采用匿名字段的方式
//Address 地址结构体
type Address struct {
Province string
City string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address //匿名字段
}
func main() {
var user2 User
user2.Name = "小王子"
user2.Gender = "男"
user2.Address.Province = "山东" // 匿名字段默认使用类型名作为字段名
user2.City = "威海" // 匿名字段可以省略
fmt.Printf("user2=%#v\n", user2)
//user2=main.User{Name:"小王子", Gender:"男", Address:main.Address{Province:"山东", City:"威海"}}
}
当访问结构体成员时会先在结构体中查找该字段,找不到再去嵌套的匿名字段中查找。
嵌套结构体内部可能存在相同的字段名。 在这种情况下,为了避免歧义需要通过指定具体的内嵌结构体字段名
//Address3 地址结构体
type Address3 struct {
Province string
City string
CreateTime string
}
//Email 邮箱结构体
type Email struct {
Account string
CreateTime string
}
//User 用户结构体
type User3 struct {
Name string
Gender string
Address3
Email
}
func main() {
var user3 User3
user3.Name = "沙河娜扎"
user3.Gender = "男"
// user3.CreateTime = "2019" //ambiguous selector user3.CreateTime
user3.Address3.CreateTime = "200000" //指定Address3结构体中的CreateTime
user3.Email.CreateTime = "2000" //指定Email结构体中的CreateTime
fmt.Printf("user2=%#v\n", user3)
//user2=main.User3{Name:"沙河娜扎", Gender:"男", Address3:main.Address3{Province:"", City:"", CreateTime:"200000"}, Email:main.Email{Account:"", CreateTime:"2000"}}
}
go语言中使用结构体也可以实现其他编程语言中的面向对象继承
//Animal 动物
type Animal struct {
name string
}
func (a *Animal) move() {
fmt.Printf("%会动:\n",a.name)
}
//Dog 狗
type Dog struct {
Feet int8
*Animal //通过嵌套匿名结构体实现继承
}
func (d *Dog) wang() {
fmt.Printf("%s会汪汪汪汪~\n",d.name)
}
func main() {
d1 := &Dog{
Feet: 4,
Animal: &Animal {
name: "乐乐"
},
}
d1.wang() //乐乐会汪汪汪
d1.move() //乐乐会动
}
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体包中可访问)
JSON是一种轻量级的数据交换格式。易于让人阅读和编写,同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,键值对组合中的键名写在前面并用双引号""
包裹,使用:
分割,然后接着值;多个健植检用英文,
分割。
//student 学生
type Student struct {
ID int
Gender string
Name string
}
//Class 班级
type Class struct {
Title string
Students []*Student
}
func main() {
c := &Class {
Title: "101"
Student: make([]*Student,0,200),
}
for i := 0;i<10;i++ {
stu := &Student{
Name: fmt.Sprintf("stu%02d",i),
Gender: "男",
ID: i,
}
c.students = append(c.Students,stu)
}
//JSON 序列化:结构体--->JSON格式的字符串
data,err :=json.Marshal(c)
if err !=nil {
fmt.Println("json marchal failed")
return
}
fmt.Printf("json:%s\n",data)
//JSON反序列化:JSON格式的字符串-->结构体
str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`
c1 := &Class{}
err = json.Unmarshal([]byte(str), c1)
if err != nil {
fmt.Println("json unmarshal failed!")
return
}
fmt.Printf("%#v\n", c1)
}
Tag
是结构体的元信息,可以在运行的时候通过反射机制读取出来。Tag
在结构体字段的后方定义,由一对反引号包裹起来,具体格式如下
`key1:"value1" key2:"value2"`
结构体tag由一个或多个键值对组成,健与值使用冒号分割,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。
注意事项:为结构体编写Tag
时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。
例如我们为Student结构体的每个字段定义json序列化时使用Tag:
//Student 学生
type Student struct {
ID int `json:"id"`
Gender string
name string
}
func main() {
s1 := Student {
ID: 1,
Gender:"男",
name: "沙河娜扎",
}
data , err := json.Marshal(s1)
if err != nil {
fmt.Println("json marshal failed")
return
}
fmt.Printf("json str:%s\n",data) //json str:{"id":1,"Gender":"男"}
}
因为slice和map这两种数据类型都包含了指向底层数组数据的指针,因此我们在需要复制它们时要特别注意。
type Person struct {
name string
age int8
dreams [] string
}
func (p *Person) SetDreams(dreams [] string) {
p.dreams = dreams
}
func main() {
p1 := Person{name: "小王子", age: 18}
data := []string{"吃饭", "睡觉", "打豆豆"}
p1.SetDreams(data)
// 你真的想要修改 p1.dreams 吗?
data[1] = "不睡觉"
fmt.Println(p1.dreams) // ?
}
正确的做法是在方法中使用传入的slice的拷贝进行结构体赋值。
func (p *Person) SetDreams(dreams []string) {
p.dreams = make([]string, len(dreams))
copy(p.dreams, dreams)
}
同样的问题也存在于返回值slice和map的情况,在实际编码过程中一定要注意这个问题。
在工程化的go语言开发项目中,go语言的源码复用建立在包(package)基础之上。下面介绍如何定义包、导出包的内容导入其他包
包与依赖管理
go语言中支持模块化的开发理念, 在go语言中使用包(package)
来支持代码模块化和代码复用。一个包是由一个或多个go源码文件(.go结尾的文件)组成,是一种高级的代码复用方案,go语言为我们提供了很多内置包,如fmt、os、io。
例如,在之前的章节中我们频繁使用了fmt
这个内置包。
package main
import "fmt"
func main() {
fmt.Println("Hello world!")
}
上面短短的几行代码涉及到了如何定义包及如何引用其他包两个内容。
我们可以根据自己需要创建自定义包。一个包可以简单理解为一个存放.go
文件的文件夹。该文件夹下面的所有.go
文件都要在非注释的第一行添加如下声明,声明该文件归属的包
package packagename
其中,
-
符号,最好与实现的功能相对应另外需要注意一个文件夹下面直接包含的文件夹只能归属一个包,同一个包文件不能在多个文件夹下。包名为==main
的包是应用程序的入口包,这种包编译后会得到一个可执行文件==,而编译不包含main
包的源代码则不会得到可执行文件。
在同一个包内部声明的标识符都位于同一个命名空间下,在不同的包内部声明的标识符就属于不同的命名空间。想要在包的外部使用包内部的标识符就需要添加包名前缀。例如fmt.Println("Hello world!"
·,就是指调用fmt
包中的Println
函数。
如果想让一个包中的标识符(如变量、常量、类型、函数等)能被外部的包使用,那么标识符必须是对外可见的(public)。在go语言中是通过标识符的首字母大小写来控制标识符的对外可见(public)不可见(private)的。在一个包内部只有首字母大写的标识符才是对外可见的。
例如我们定义一个名为demo的包,在其中定义了若干标识符。在另外一个包中并不是所有的标识符都能通过demo.前缀访问到,因为只有那些首字母是大写的标识符才是对外可见的。
package demo
import "fmt"
//包级别标识符可见性
//num 定义一个全局整型变量
//首字母小写,对外不可见(只能在当前包内使用)
var num = 100
//Mode 定义一个常量
//首字母大写,对外不可见(可在其他包中使用)
const Mode=1
//person 定义一个代表人的结构体
//首字母小写,对外不可见(只能在当前包内使用)
type person struct {
name string
Age int
}
//Add 返回两个整数和的函数
//首字母大写,对外可见(可在其他包中使用)
func Add(x,y int)int {
return x+y
}
//sayHi 打招呼的函数
//首字母小写,对外不可见(只能在当前包内使用)
func sayHi() {
var myName = "七米"
fmt.Println(myName)
}
同样的规则也适用于结构体,结构体中可导出字段的字段名称必须首字母大写。
type Student struct {
Name string // 可在包外访问的方法
class string // 仅限包内访问的字段
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uKQJhOrT-1656895196624)(go基础.assets/image-20220627173154569.png)]
要在当前包中使用另一个包的内容就需要使用import
关键字引入这个包,并且import语句通常放在文件的开头,package声明语句的下方。完整引入声明语句格式如下:
import importname "path/to/package"
其中,
一个go源码文件中可以同时引入多个包,例如:
import "fmt"
import "net/http"
import "os"
当然可以使用批量引入的方式
import (
"fmt"
"net/http"
"os"
)
当引入的多个包中存在相同的包名或者想自行为某个引入的包设置一个新包名时,都需要通过importname
指定一个在当前文件夹中使用的新包名。例如,在引入fmt
包时为其指定一个新包名f
import f "fmt"
这样在当前这个文件夹中就可以通过使用f
来调用fmt
包中的函数了
f.Println("Hello world")
如果引入一个包的时候为其设置了一个特殊_
作为包名,那么这个包的引入方式就称为匿名引入。一个包被匿名引入的目的主要时为了加载这个包,从而使得这个包中的资源得以初始化。被匿名引入的包中init
函数将被执行且执行一遍。
import _"github.com/go-sql-driver/mysql"
匿名引入的包与其他方式导入的包一样都会被编译到可执行文件中。
需要注意的时,go语言中不允许引入包却不在代码中使用这个包的内容,如果引入了未使用的包则会触发编译错误。
在每一个go源文件中,都可以定义任意个如下格式的特殊函数:
func main() {
//...
}
这种特殊的函数不接受任何参数也没有任何返回值,我们不能在代码中主动调用它。当程序启动的时候,init函数会按照它们声明的顺序自动执行。
一个包的初始化过程是按照代码中引入的顺序来执行的,所有在该包中声明的init函数都将被串行调用并且调用执行一次。每一个包初始化的时候都是先执行依赖的包中声明的init函数在执行当前包中声明的init函数,确保在程序的main函数开始执行时所有的依赖包都已经初始化完成。
总结
从下面的示例中我们就可以看出包级别变量的初始化会先于init
初始化函数。
package main
import "fmt"
var x int8 = 10
const pi = 3.14
func init() {
fmt.Println("x:",x)
fmt.Println("pi:",pi)
sayHi()
}
func sayHi() {
fmt.Println("Hello world")
}
func main() {
fmt.Println("你好,世界")
}
go module是go1.11版本发布的依赖管理方案,从go 1.14版本开始推荐在生产环境使用,于go 1.16版本默认开启。
go module相关命令
命令 | 介绍 |
---|---|
go mod init | 初始化项目依赖,生成go.mod文件 |
go mod download | 根据go.mod文件下载依赖 |
go mod tidy | 比对项目文件中引入的依赖与go.mod进行比对 |
go mod graph | 输出依赖关系图 |
go mod edit | 编辑go.mod文件 |
go mod vendor | 将项目的所有依赖导出至vendor目录 |
go mod verify | 检验一个依赖包是否被篡改过 |
go mod why | 解释为什么需要某个依赖 |
go语言在go module的过渡阶段提供了GO11MODULE
这个环境变量来作为是否启用go module功能的开关,go 1.16之后go module已经默认开启。
GOPROXY
GOPROXY 的默认值是:https://proxy.golang.org,direct
,由于某些原因国内无法正常访问该地址,所以我们通常需要配置一个可访问的地址。目前社区使用比较多的有两个https://goproxy.cn
和https://goproxy.io
,当然如果你的公司有提供GOPROXY地址那么就直接使用。设置GOPAROXY的命令如下:
go env -w GOPROXY=https://goproxy.cn,direct
GOPROXY 允许设置多个代理地址,多个地址之间需使用英文逗号 “,” 分隔。最后的 “direct” 是一个特殊指示符,用于指示 Go 回源到源地址去抓取(比如 GitHub 等)。当配置有多个代理地址时,如果第一个代理地址返回 404 或 410 错误时,Go 会自动尝试下一个代理地址,当遇见 “direct” 时触发回源,也就是回到源地址去抓取。
GOPRIVATE
设置了GOPROXY之后,go命令就会从配置的代理地址拉取和效验依赖包。当我们在项目中引入了非公开的包(公司内部git仓库或github私有仓库等),此时便无法正常从代理拉取到这些非公开的依赖包,这个时候就需要配置GOPEIVATE环境变量。GOPRIVATE用来告诉go命令哪些仓库属于私有仓库,不必通过代理服务器拉取和效验。
GOPRIVATE 的值也可以设置多个,多个地址之间使用英文逗号 “,” 分隔。我们通常会把自己公司内部的代码仓库设置到 GOPRIVATE 中,例如:
$ go env -w GOPRIVATE="git.mycompany.com"
这样在拉取以git.mycompany.com
为路径前缀的依赖包时就能正常拉取了。
此外,如果公司内部自建了 GOPROXY 服务,那么我们可以通过设置 GONOPROXY=none
,允许通内部代理拉取私有仓库的包。
Go语言基础之包 | 李文周的博客 (liwenzhou.com)
Go语言基础之接口 | 李文周的博客 (liwenzhou.com)
package main
import "fmt"
// 为什么需要接口
type dog struct{}
//定义一个方法 方法区别于函数,要有一个参数的接收者
func (d dog) say() {
fmt.Println("汪汪哇")
}
type cat struct{}
func (c cat) say() {
fmt.Println("喵喵喵喵")
}
//定义一个抽象的类型,只要实现了say这个方法类型都可以称为sayer类型
type sayer interface {
say()
}
type person struct {
name string
}
func (p person)say() {
fmt.Println("啊啊啊啊")
}
//接口不管你是什么 只管你要实现什么方法
//打的函数 接收一个参数,不管是什么类型
func da(arg sayer) {
arg.say()
}
func main() {
c1 := cat{}
da(c1)
d1 := dog{}
da(d1)
p1:=person{
name:"娜扎",
}
da(p1)
}
package main
import "fmt"
type mover interface {
move()
}
type person struct {
name string
age int8
}
//使用值接收者实现接口:类型的值和类型的指针,都能够保存到接口变量中
/*func (p person) move() {
fmt.Printf("%s再跑...\n", p.name)
}*/
//使用指针接收者实现接口:只有类型指针能够保存到接口变量中
func (p *person)move() {
fmt.Printf("%s再跑...\n", p.name)
}
func main() {
var m mover
p1 := person{ //p1是值类型
name: "雄安王子",
age: 30,
}
p2 := &person{ //p2是person类型的指针
name: "娜扎",
age: 18,
}
m = p1 //??使用指针接收者实现接口:类型的值,不能赋值给(实现)接口
m = p2
m.move()
fmt.Println(m)
}
同一个类型可以实现多个接口,不同多个类型也可以实现同一个接口
package main
import "fmt"
//接口的嵌套
type animal interface {
mover
sayer
}
//定义接口
type mover interface {
move()
}
type sayer interface {
say()
}
//定义结构体
type person struct {
name string
age int8
}
//实现接口和接口方法
//使用值接收者实现接口:类型的值和类型的指针,都能够保存到接口变量中
/*func (p person) move() {
fmt.Printf("%s再跑...\n", p.name)
}*/
//使用指针接收者实现接口:只有类型指针能够保存到接口变量中
func (p *person) move() {
fmt.Printf("%s再跑...\n", p.name)
}
func (p *person) say() {
fmt.Printf("%s再叫···\n", p.name)
}
func main() {
// 使用接口,定义一个mover类型的变量
var m mover
/* p1 := person{ //p1是值类型
name: "雄安王子",
age: 30,
}*/
p2 := &person{ //p2是person类型的指针
name: "娜扎",
age: 18,
}
//m = p1 //??使用指针接收者实现接口:类型的值,不能赋值给接口
m = p2
m.move()
fmt.Println(m)
// 使用接口,定义一个sayer类型的变量
var s sayer
s = p2
s.say()
fmt.Println(s)
var a animal
a = p2
a.move()
a.say()
fmt.Println(a)
}
package main
import "fmt"
//空接口
//接口中没有定义任何需要实现的方法时,该接口就是一个空接口
//任意类型都实现了空接口--> 空接口变量可以存储任意值
//空接口一般不需要提前定义
type xxx interface {
}
//空接口的应用
//1. 空接口类型作为i函数的参数
//2. 空接口可以作为map的value
func main() {
var x interface{} //定义一个空接口变量x
x = "hello"
x = 100
x = false
fmt.Println(x)
//value 是不固定类型,这里我们使用空接口
var m = make(map[string]interface{}, 16)
m["name"] = "娜扎"
m["age"] = 18
m["hobby"] = []string{"篮球", "足球", "双色球"}
fmt.Println(m)
}
package main
import "fmt"
func main() {
var x interface{}
x = 100
x = false
x = "hello"
ret, ok := x.(string)
if !ok {
fmt.Println("猜错了")
} else {
fmt.Println("是字符串", ret)
}
//使用switch断言
switch v := x.(type) {
case string:
fmt.Printf("是字符串类型,value:%v",v)
case bool:
fmt.Printf("是布尔类型,value:%v",v)
case int:
fmt.Printf("是int类型,value:%v",v)
default:
fmt.Printf("猜不到了,value:%v",v)
}
}
package main
import (
"fmt"
"reflect"
)
func reflectType(x interface{}) {
//我不知道别人调用这个函数的时候会传入什么类型的变量
// 1. 方式1:通过类型断言
// 2. 方式2:借助反射
obj := reflect.TypeOf(x)
fmt.Println(obj)
fmt.Printf("%T\n", obj)
}
func reflectValue(x interface{}) {
v := reflect.ValueOf(x)
fmt.Printf("%v,%T\n", v, v)
//如何得到一个v换入时候类型的变量
k := v.Kind()
fmt.Println(k)
switch k {
case reflect.Float32:
//把反射取到的值切换成一个float32类型的变量
ret := float32(v.Float())
fmt.Println(ret)
case reflect.Int32:
ret := int32(v.Int())
fmt.Printf("%v %T\n", ret, ret)
}
}
func reflectSetValue(x interface{}) {
v := reflect.ValueOf(x)
//Elem()用来根据指针取对应的值
k := v.Elem().Kind()
switch k {
case reflect.Int32:
v.Elem().SetInt(100)
case reflect.Float32:
v.Elem().SetFloat(3.21)
}
}
type Cat struct {
}
type Dog struct {
}
func main() {
/* var a float32 = 1.234
reflectType(a)
var b int8 = 10
reflectType(b)
var c Cat
reflectType(c)
var d Dog
reflectType(d)*/
/*var aa int32 = 100
reflectValue(aa)*/
var aaa int32 = 10
reflectSetValue(&aaa)
fmt.Println(aaa)
}
持续更新,第二遍学习中···