Go
vscode配置go开发环境
1.下载vscode
https://code.visualstudio.com/
2.安装sdk
https://golang.google.cn/dl/
进入这个界面后找到对应版本 go版本.windows-amd64.zip
安装路径不能有中文或者特殊符号
3.测试sdk安装是否成功
在bin目录
go version 回车
输出 go version go版本 windows/amd64
4.配置golang环境变量
1.系统变量 新建
变量名:GOROOT
变量值:sdk的目录
2.系统变量 编辑Path变量
Path变量中新建 %GOROOT%\bin
3.系统变量 新建GOPATH变量
这个变量是go项目存放的路径 自己确定位置
例:D:\goproject
go的目录结构
goproject
src
go_code
project01
project02
project03
然后,每个project下面有main和 package
对目录进行说明
go文件后缀
是 .go
package main
表示hello.go文件所在包是main 在go中,每个文件必须归属一个包
impor “fmt”
表示引入一个包,包名为fmt,引入后,就可以使用fmt包的函数,比如:fmt.Println
funcmain(){}
func是一个关键字,表示一个函数。main是函数名,是一个主函数,即我们程序的入口。
fmt.Println(“hello”)
表示调用fmt包的函数Println输出“hello,world”
gobuild
通过该命令对该go文件进行编译,生成.exe文件.
运行hello.exe文件即可
gorun
注意:通过该命令可以直接运行hello.go程序
但是不规范,而且需要对源码编译,很慢
[类似执行一个脚本文件的形式]
vscode常见错误
The “go-outline” command is not available. Run "go get -v github.com/ramya-r
解决方法:
使用win+R输入cmd,输入以下命令行:
// 开启代理设置
go env -w GO111MODULE=on
// 设置代理源
go env -w GOPROXY=https://goproxy.io,direct
重启VScode ,打开创建好的go文件,当再次提示The “go-outline” command is not available. Run "go get -v github.com/ramya-r时,直接点击install all静静等待安装好就行了。
package main爆红
go: go.mod file not found in current directory or any parent directory; see ‘go help modules’
go env -w GO111MODULE=auto
重启vscode问题解决
go基础
- go源文件以“.go”为拓展名
- 执行入口是main函数
- 严格区分大小写
- go方法由一条条语句组成,每条语句后面不需要分号(go自动加)
- 一行写多条语句,中间需要加分号隔开
- go定义的变量或者import的包如果没有用到,代码不能编译通过
- 大括号成对出现
转义字符
-
\t:表示一个制表符,通常使用它可以排版。
-
\n:换行符
-
\\
:一个\ -
\"
·:一个" -
\r:一个回车fmt.Println(“天龙八部雪山飞狐\r张飞”);
这个输出的是张飞八部雪山飞狐
因为这是回车 和换行不一样 它回车后只是从头开始继续覆盖输出,也就是张飞覆盖了原来的天龙
但是vscode里面不是这样 goland和他俩也都不一样 迷惑
注释
-
单行注释 //
-
多行注释
/*
注释内容
*/
(注:shift+tab 取消无序列表的不缩进)
api中文文档
https://studygolang.com/pkgdoc
变量
变量=变量名+值+数据类型
格式
var 变量名 数据类型
例:
var num int
如果直接赋值可以省略类型
比如:
var num=10
注意点
1.默认值
指定变量类型声明后如果不赋值,将会使用默认值,比如int的默认值是0
2.类型推导
赋值后会根据值自行判定变量类型
例:
var num=10
3.省略var
省略var,:=左侧的变量不能是已经声明过的
name:=“tom”
这种等价于
var name string
name=“tom”
4.多变量声明
三种方式
var n1,n2,n3 int
var n1,name,n3=100,”tom”,555
n1,name,n3:=100,”tom”,666
5.全局变量
一次性声明多个全局变量
例:
var n1=100
var n2=200
var name=“jack”
改成一次性声明
var(
n1=100
n2=200
name=“jack”
)
6.重新赋值
变量在他的作用域下面可以重新赋值,但是不能改变类型
数据类型
基本介绍
整数:
int,int8,int16,int32,int64,uint,uint8,uint16,uint32,uint64,byte
浮点数:
float32,float64
字符型:
没有专门的字符型,用byte来保存单个字母字符
布尔型:
bool
字符串:
string
派生/复杂数据类型:
指针、数组、结构体、管道、函数、切片、接口、map
后面会详细说
基本数据类型默认值
整型:0
浮点型:0
字符串:“”
布尔类型:false
基本数据类型的相互转换
Golang和java/c不同,Go在不同类型的变量之间赋值时需要显式转换。也就是说Golang中数据类型不能自动转换。
公式
表达式T(v)将值v转换为类型T
T:就是数据类型,比如int32,int64,float32等等
v:就是需要转换的变量
举例
var i int32=100
//将i变成float类型
var n=float32(i)
注意事项
- Go中,数据类型的转换可以是从表示范围小–>表示范围大,也可以范围大—>范围小
- 被转换的是变量存储的数据(即值),变量本身的数据类型并没有变化!
- 在转换中,比如将int64转成int8【-128—127】,编译时不会报错,只是转换的结果是按溢出处理,和我们希望的结果不一样。因此在转换时,需要考虑范围.
转string类型
方式一:
fmt.Sprintf("%参数",表达式)
参数需要和表达式的数据类型相匹配
fmt.Sprintf()…会返回转换后的字符串
var num1 int=99
var num2 float64=23.456
var b bool=true
var c byte='h'
var str string
//转换
str=fmt.Sprintf("%d",num1)
str=fmt.Sprintf("%f",num2)
str=fmt.Sprintf("%t",b)
str=fmt.Sprintf("%c",c)
//验证
fmt.Printf("str type %T str=%q",str,str)
//上面每个都可以转换完之后验证一下
方式二:
strconv包的函数
var num3 int=99
var num4 float64=23.456
var b2 bool=true
str=strconv.FormatInt(int64(num3),10)
//10:转换的进制基数
str=strconv.FormatFloat(num4,'f',10,64)
//'f'格式 10:表示小数位保留10位 64:表示小数是双精度
str=strconv.FormatBool(b2)
//还有一个Itoa函数 将数字转换成对应的字符串类型的数字。
//例:
var num5 int64=4567
str=strconv.Itoa(int(num5))
string转基本数据类型
var str string = "true"
var b bool
// b, _ = strconv.ParseBool(str)
// 说明:
// 1、strconv.ParseBool(str) 函数会返回两个值 (value bool, err error)
// 2、因为我只想获取到 value bool ,不想获取 err 所以我使用——忽略
b,_ = strconv.ParseBool(str)
fmt.Printf("b type %T b=%v\n", b, b)
//----------------------------------------------
//----------------------------------------------
var str2 string = "1234590"
var n1 int64
var n2 int
n1,_ = strconv.ParseInt(str2, 10, 64)
//10:转换的进制基数
//64:返回的int类型
n2 = int(n1)
fmt.Printf("n1 type %T n1=%v\n", n1, n1)
fmt.Printf("n2 type %T n2=%v\n", n2, n2)
//----------------------------------------------
//----------------------------------------------
var str3 string = "123.456"
var f1 float64
f1,_ = strconv.ParseFloat(str3, 64)
fmt.Printf("f1 type %T f1=%v\n", f1, f1)
//----------------------------------------------
//----------------------------------------------
//还有个atoi函数,将字符串转成对应数字
i,_ := strconv.Atoi(s)
因为上面我们返回的是int64或者float64,希望得到int32或者float32需要自己进行转换
int32(返回值)或者float(返回值)
注意事项
在将String类型转成基本数据类型时,要确保String类型能够转成有效的数据,比如我们可以把"123",转成一个整数,但是不能把"hello"转成一个整数,如果这样做,Golang直接将其转成0,其它类型也是一样的道理.float=>0 bool=>false
整数类型
有符号型和无符号型 占用储存空间和表数范围的差异
存储字符用byte
有符号和无符号,int、uint的大小和系统有关
Golang整形默认声明为int
比如
var n=100 // n的类型是int
unsafe.sizeof
在程序查看某个变量的字节大小和数据类型
保小不保大
Golang程序中整型变量在使用时,遵守保小不保大的原则,即:在保证程序正确运行下,尽量使用占用空间小的数据类型
例如:
var age byte=90
bit:计算机中的最小存储单位。byte:计算机中基本存储单元。
二进制再详细说
1byte=8bit
浮点类型
单精度:float32
双精度:float64
存放形式
关于浮点数在机器中存放形式的简单说明,浮点数=符号位+指数位+尾数位说明:浮点数都是有符号的.
精度损失
尾数部分可能丢失,造成精度损失
说明:
float64的精度比float32的要准确.说明:如果我们要保存一个精度高的数,则应该选用float64
细节
-
Golang浮点类型有固定的范围和字段长度,不受具体OS(操作系统)的影响
-
Golang的浮点型默认声明为float64类型。
-
浮点型常量有两种表示形式
十进制数形式:
如:5.12 .512(必须有小数点) .512也就是0.512
科学计数法形式:
如:5.1234e2=5.12*10的2次方 5.12E-2=5.12/10的2次方
-
通常情况下,应该使用float64,因为它比float32更精确。
字符类型
- Golang中没有专门的字符类型,如果要存储单个字符(字母),一般使用byte来保存。
- 字符串就是一串固定长度的字符连接起来的字符序列。
- Go的字符串是由单个字节连接起来的。也就是说对于传统的字符串是由字符组成的,而Go的字符串不同,它是由字节组成的。
举例
- 如果我们保存的字符在ASCII表的,比如[0-1,a-z,A-Z…]直接可以保存到byte
- 如果我们保存的字符对应码值大于255,这时我们可以考虑使用int类型保存
- 如果我们需要安装字符的方式输出,这时我们需要格式化输出,即fmt.Printf(“%c”,c1).
细节
- 字符常量是用单引号(’’)括起来的单个字符。例如:varc1byte='a’varc2int='中’varc3byte=‘9’
- Go中允许使用转义字符’\’来将其后的字符转变为特殊字符型常量。例如:varc3char=‘\n’//’\n’表示换行符
- Go语言的字符使用UTF-8编码,如果想查询字符对应的utf8码值http://www.mytju.com/classcode/tools/encode_utf8.asp英文字母-1个字节汉字-3个字节
- 在Go中,字符的本质是一个整数,直接输出时,是该字符对应的UTF-8编码的码值。
- 可以直接给某个变量赋一个数字,然后按格式化输出时%c,会输出该数字对应的unicode字符
- 字符类型是可以进行运算的,相当于一个整数,因为它都对应有Unicode码
本质
-
字符型存储到计算机中,需要将字符对应的码值(整数)找出来
存储:字符—>对应码值---->二进制–>存储
读取:二进制---->码值---->字符–>读取
-
字符和码值的对应关系是通过字符编码表决定的(是规定好)
-
Go语言的编码都统一成了utf-8。非常的方便,很统一,再也没有编码乱码的困扰了
布尔类型
- 只允许true或者false
- 占用一个字节
字符串类型
字符串就是一串固定长度的字符连接起来的字符序列。Go的字符串是由单个字节连接起来的。Go语言的字符串的字节使用UTF-8编码标识Unicode文本
细节
- Go语言的字符串的字节使用UTF-8编码标识Unicode文本,这样Golang统一使用UTF-8编码,中文乱码问题不会再困扰程序员。
- 字符串一旦赋值了,字符串就不能修改了:在Go中字符串是不可变的。
表示形式
- 双引号,会识别转义字符
- 反引号,以字符串的原生形式输出,包括换行和特殊字符,可以实现防止攻击、输出源代码等效果
字符串拼接
var str=“hello”+“world”
str+=“haha”
当拼接很长的字符串时,可以换行,但是行的末尾必须是+号
指针
存址
获取变量的地址,用&,比如:var num int,获取num的地址:&num
var ptr *int=&num
//*ptr 解引用获得地址的值
细节
- 值类型,都有对应的指针类型,形式为数据类型,比如int的对应的指针就是int,float32对应的指针类型就是*float32,依次类推。
- 值类型包括:基本数据类型int系列,float系列,bool,string、数组和结构体struct
值类型和引用类型
- 值类型:基本数据类型int系列,float系列,bool,string、数组和结构体struct
- 引用类型:指针、slice切片、map、管道chan、interface等都是引用类型
特点
-
值类型:变量直接存储值,内存通常在栈中分配
-
引用类型:变量存储的是一个地址,这个地址对应的空间才真正存储数据(值),内存通常在堆上分配,当没有任何变量引用这个地址时,该地址对应的数据空间就成为一个垃圾,由GC来回收
-
栈区:值类型数据,通常是在栈区
堆区:引用类型,通常在堆区分配空间
标识符
- 由26个英文字母大小写,0-9,_组成
- 数字不可以开头
- Golang中严格区分大小写
- 标识符不能包含空格
- 下划线"_"本身在Go中是一个特殊的标识符,称为空标识符。可以代表任何其它的标识符,但是它对应的值会被忽略(比如:忽略某个返回值)。所以仅能被作为占位符使用,不能作为标识符使用
- 不能以系统保留关键字作为标识符(一共有25个),比如break,if等等…
注意:
int//ok,我们要求大家不要这样使用
float32//ok,我们要求大家不要这样使用
命名注意事项
-
包名:保持package的名字和目录保持一致,尽量采取有意义的包名,简短,有意义,不要和标准库不要冲突fmt
-
变量名、函数名、常量名:采用驼峰法举例:
var stuName string=“tom”形式:xxxYyyyyZzzz
var goodPrice float32=1234.5
-
如果变量名、函数名、常量名首字母大写,则可以被其他的包访问;如果首字母小写,则只能在本包中使用
(注:可以简单的理解成,首字母大写是公开的,首字母小写是私有的)在golang没有public,private等关键字
系统保留关键字和预定义标识符
自行百度,可能会随时变化
运算符
算数运算符
+,-,*,/,%,++,–
注意%:
fmt.Println("10%3=",10%3)//=1
fmt.Println("-10%3=",-10%3)
//=-10-(-10)/3*3=-10-(-9)=-1
fmt.Println("10%-3=",10%-3)//=1
fmt.Println("-10%-3=",-10%-3)//=-1
注意事项
-
对于除号"/",它的整数除和小数除是有区别的:整数之间做除法时,只保留整数部分而舍弃小数部分。例如:x:=19/5,结果是3
-
当对一个数取模时,可以等价a%b=a-a/b*b,这样我们可以看到取模的一个本质运算
-
Golang的自增自减只能当做一个独立语言使用
不可以这样:
a=i++ a=i-- if(i++>0) //这三种都是错误的
-
Golang的++和–只能写在变量的后面,不能写在变量的前面,即:只有a++a–没有++a–a
没有前置++和–
-
Golang的设计者去掉c/java中的自增自减的容易混淆的写法,让Golang更加简洁,统一。(强制性的)
关系运算符
== ,!= ,< ,> ,<= ,>=
逻辑运算符
&& , || , !(逻辑 非 运算符)
注意:
- &&也叫短路与:如果第一个条件为false,则第二个条件不会判断,最终结果为false
- ||也叫短路或:如果第一个条件为true,则第二个条件不会判断,最终结果为true
赋值运算符
=, +=, -=, *=, /=, %=
下面这些在二进制再说
<<=, >>=, &=, ^=, |=
位运算符
&, |, ^, <<, >>
同样在二进制
其他运算符
& :返回变量存储的地址
*: 指针变量
Go不支持三元运算符
//传统三元运算
n=i>j?i:j
//Go中
if i>j{
n=i
}else{
n=j
}
运算符优先级
1:括号,++,–
2:单目运算
3:算术运算符
4:移位运算
5:关系运算符
6:位运算符
7:逻辑运算符
8:赋值运算符
9:逗号
键盘输入
调用fmt包的fmt.Scanln()或者fmt.Scanf()
方式一:
var name string
var age byte
var sal float32
var isPass bool
fmt.Scanln(&name)
fmt.Scanln(&age)
fmt.Scanln(&sal)
fmt.Scanln(&isPass)
方式二:
fmt.Scanf("%s %d %f %t",&name,&age,&sal,&isPass)
进制
另有博客记录 插个目录
程序流程控制
判断
if 表达式1{
执行1
}else if 表达式2{
执行2
}else{
执行其它
}
if表达式不能为赋值语句
switch
golang里不需要break
- switch的执行的流程是,先执行表达式,得到值,然后和case的表达式进行比较,如果相等,就匹配到,然后执行对应的case的语句块,然后退出switch控制。
- 如果switch的表达式的值没有和任何的case的表达式匹配成功,则执行default的语句块。执行后退出switch
- golang的case后的表达式可以有多个,使用逗号间隔.
- golang中的case语句块不需要写break,因为默认会有,即在默认情况下,当程序执行完case语句块后,就直接退出该switch控制结构。
switch 表达式{
case 表达式1,表达式2,...:
执行语句1
case 表达式3,表达式4,...:
执行语句2
case ...:
...
default:
执行语句
}
注意:
- case/switch后是一个表达式(即:常量值、变量、一个有返回值的函数等都可以)
- case后的各个表达式的值的数据类型,必须和switch的表达式数据类型一致
- case后面可以带多个表达式,使用逗号间隔。比如case表达式1,表达式2…
- case后面的表达式如果是常量值(字面量),则要求不能重复
- case后面不需要带break,程序匹配到一个case后就会执行对应的代码块,然后退出switch,如果一个都匹配不到,则执行default
- default语句不是必须的.
- switch后也可以不带表达式,类似if–else分支来使用
var age int = 10
switch{
case age==10:
fmt.Println...
case age==20:
fmt.Println...
default:
....
}
- switch后也可以直接声明/定义一个变量,分号结束,不推荐。
switch grade:=90{
case grade>=90:
..
...
}
- switch穿透-fallthrough,如果在case语句块后增加fallthrough,则会继续执行下一个case,也叫switch穿透(默认穿透一层)
var age int = 10
switch{
case age==10:
fmt.Println...
fallthrough
case age==20:
fmt.Println...
fallthrough
default:
....
}
- TypeSwitch:switch语句还可以被用于type-switch来判断某个interface变量中实际指向的变量类型(还没有学interface,先体验一把)
var x interface{}
var y=10.0
x=y
switch i:=x.(type){
case nil:
fmt.Printf("x的类型是:%T",i)
case int:
fmt.Printf("x的类型是int型")
...
}
switch和if比较
- 如果判断的具体数值不多,而且符合整数、浮点数、字符、字符串这几种类型。建议使用swtich语句,简洁高效。
- 其他情况:对区间判断和结果为bool类型的判断,使用if,if的使用范围更广。
for
方式一:
for i:=1;i<=10;i++{
fmt.Println("hello world",i)
}
方式二:
i:=1
for i<=10{
fmt.Println("hello world",i)
i++
}
方式三:
//死循环
for{
执行语句
}
//这种写法等价于下面这种 通常需要配合if和break语句使用
for ; ;{
}
循环条件是返回一个布尔值的表达式
Golang提供for-range的方式,可以方便遍历字符串和数组(注:数组的遍历,我们放到讲数组的时候再讲解),案例说明如何遍历字符串
方式一:
var str string="hello world"
for i:=0;i<len(str);i++{
fmt.Printf("%c",str[i])
}
方式二:for range
var str string="hello world"
for index,val:=range str{
fmt.Printf("index=%d,val=%c",index,val)
}
注意:
如果我们的字符串含有中文,那么传统的遍历字符串方式,就是错误,会出现乱码。原因是传统的对字符串的遍历是按照字节来遍历,而一个汉字在utf8编码是对应3个字节。
如何解决? 需要要将str转成[]rune切片.
var str string="hello world,北京"
str2:= []rune(str)
for i:=0;i<len(str2);i++{
fmt.Printf("%c ",str2[i])
}
不过对于for-range遍历方式而言,是按照字符方式遍历。因此如果有字符串有中文,也是ok
var str string="hello world,北京"
for index,val:=range str{
fmt.Printf("index=%d,val=%c\n",index,val)
}
Go语言没有while和do…while语法,这一点需要同学们注意一下,如果我们需要使用类似其它语言(比如java/c的while和do…while),可以通过for循环来实现其使用效果。
continue
- continue语句用于结束本次循环,继续执行下一次循环。
- continue语句出现在多层嵌套的循环语句体中时,可以通过标签指明要跳过的是哪一层循环,这个和前面的break标签的使用的规则一样.
goto
- Go语言的goto语句可以无条件地转移到程序中指定的行。
- goto语句通常与条件语句配合使用。可用来实现条件转移,跳出循环体等功能
- 在Go程序设计中一般不主张使用goto语句,以免造成程序流程的混乱,使理解和调试程序都产生困难
goto label:
...
label:statement
例:
var n = 30
if n >20{
goto label2
}
fmt.Println("1")
fmt.Println("2")
fmt.Println("3")
label2:
fmt.Println("4")
fmt.Println("5")
fmt.Println("6")
return
- return使用在方法或者函数中,表示跳出所在的方法或函数,在讲解函数的时候,会详细的介绍
- 如果return是在普通的函数,则表示跳出该函数,即不再执行函数中return后面代码,也可以理解成终止函数。
- 如果return是在main函数,表示终止main函数,也就是说终止程序。
包
- 在实际的开发中,我们往往需要在不同的文件中,去调用其它文件的定义的函数,比如main.go中,去使用utils.go文件中的函数,如何实现?-> 包
- 现在有两个程序员共同开发一个Go项目,程序员xiaoming希望定义函数Cal,程序员xiaoqiang也想定义函数也叫Cal。两个程序员为此还吵了起来,怎么办? -> 包
- go的每一个文件都是属于一个包的,也就是说go是以包的形式来管理文件和项目目录结构
包的本质实际上就是创建不同的文件夹,来存放程序文件
作用
- 区分相同名字的函数、变量等标识符
- 当程序文件很多时,可以很好的管理项目
- 控制函数、变量等访问范围,即作用域
说明
-
打包基本语法:package 包名
-
引入包基本语法:import “包的路径”
细节
-
在给一个文件打包时,该包对应一个文件夹,比如这里的utils文件夹对应的包名就是utils,文件的包名通常和文件所在的文件夹名一致,一般为小写字母。
-
当一个文件要使用其它包函数或变量时,需要先引入对应的包
引入方式1:
import “包名”
引入方式2:
import ( “包名” “包名” )
package指令在文件第一行,然后是import指令。
在import包时,路径从$GOPATH的src下开始,不用带src,编译器会自动从src下开始引入
-
为了让其它包的文件,可以访问到本包的函数,则该函数名的首字母需要大写,类似其它语言的public,这样才能跨包访问。
-
在访问其它包函数,变量时,其语法是包名.函数名
-
如果包名较长,Go支持给包取别名,注意细节:取别名后,原来的包名就不能使用了
-
在同一包下,不能有相同的函数名(也不能有相同的全局变量名),否则报重复定义
-
如果你要编译成一个可执行程序文件,就需要将这个包声明为main,即packagemain.这个就是一个语法规范,如果你是写一个库,包名可以自定义
函数
func 函数名 (形参列表) (返回值列表) {
执行语句..
return 返回值列表
}
函数也可以没有返回值
例:简易计算器
func Cal(n1 float64,n2 float64,operator byte) float64{
var res float64
switch operator{
case '+':
res=n1+n2
...
default:
fmt.Println("操作符有误")
}
return res
}
细节
- 在调用一个函数时,会给该函数分配一个新的空间,编译器会通过自身的处理让这个新的空间和其它的栈的空间区分开来
- 在每个函数对应的栈中,数据空间是独立的,不会混淆
- 当一个函数调用完毕(执行完毕)后,程序会销毁这个函数对应的栈空间。
return
Go函数支持返回多个值
func 函数名 (形参列表) (返回值类型列表){
语句..
return 返回值列表
}
- 当返回多个值在接收时,如果希望忽略某个返回值,使用_
- 如果返回值只有一个,返回值类型列表的括号可以省略,即直接写类型
例:
func getSumAndSub(n1 int,n2 int) (int,int){
sum:=n1+n2
sub:=n1-n2
return sum,sub
}
func main(){
ret1,ret2:=getSumAndSub(1,2)
fmt.Printf("ret1=%v,ret2=%v",ret1,ret2)
//如果希望忽略某个返回值
_,res3=getSumAndSub(1,2)
fmt.Printf("ret3=%v",ret3)
}
注意事项
- 函数的形参列表可以是多个,返回值列表也可以是多个。
- 形参列表和返回值列表的数据类型可以是值类型和引用类型。
- 函数的命名遵循标识符命名规范,首字母不能是数字,首字母大写该函数可以被本包文件和其它包文件使用,类似public,首字母小写,只能被本包文件使用,其它包文件不能使用,类似priva
- 函数中的变量是局部的,函数外不生效
- 基本数据类型和数组默认都是值传递的,即进行值拷贝。在函数内修改,不会影响到原来的值
- 如果希望函数内的变量能修改函数外的变量(指的是默认以值传递的方式的数据类型),可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用
- Go函数不支持函数重载
- 在Go中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用
func getSumAndSub(n1 int,n2 int) (int,int){
sum:=n1+n2
sub:=n1-n2
return sum,sub
}
func main(){
a:=getSumAndSub
res1,res2:=a(1,2)
//等价于res1,res2:=getSumAndSub(1,2)
}
9. 函数既然是一种数据类型,因此在Go中,函数可以作为形参,并且调用
10. 为了简化数据类型定义,Go支持自定义数据类型
基本语法:type 自定义数据类型名 数据类型 //理解:相当于一个别名
案例1:type myInt int //这时myInt就等价int来使用了
案例2:type mySum func(int,int) int
//这时mySum就等价一个函数类型func(int,int)int
11. 支持对函数返回值命名
12. 使用_标识符,忽略返回值
13. Go支持可变参数
//支持0到多个参数
func sum(args...int)sum int{
}
//支持1到多个参数
func sum(n1 int,args...int)sum int{
}
- args是slice切片,通过args[index]可以访问到各个值
- 如果一个函数的形参列表中有可变参数,则可变参数需要放在形参列表最后
- 例:
//计算1到多个int的和
func sum(n1 int,args...int)int{
sum:=n1
for i:=0;i<len(args);i++{
sum+=args[i]
}
return sum
}
小案例:
type mySum func (int, int) int
func sum(n1 int, n2 int) int{
return n1+n2
}
func sum2(n1,n2, n3 int) int {
return n1 +n2
}
//使用type自定义数据类型来简化定义
func myFunc(funcVar mySum, num1 int, num2 int) int {
return funcVar(num1, num2)
}
func main(){
a := sum
b := sum2
fmt.println(myFunc(a,1,2))//ok
fmt.PrintIn(myFunc(b,1,2)) //error
}
//代码有无错误,为什么?
//fmt.Println(myFunc(b, 1,2)错误
//原因是类型不匹配.因为不能把func sum2(n1, n2, n3 int) int赋给func (int, int)int
递归
一个函数在函数体内又调用了本身,我们称为递归调用
重要原则
- 执行一个函数时,就创建一个新的受保护的独立空间(新函数栈)
- 函数的局部变量是独立的,不会相互影响
- 递归必须向退出递归的条件逼近,否则就是无限递归
- 当一个函数执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁,同时当函数执行完毕或者返回时,该函数本身也会被系统销毁
init函数
每一个源文件都可以包含一个init函数,该函数会在main函数执行前,被Go运行框架调用,也就是说init会在main函数前被调用
细节
- 如果一个文件同时包含全局变量定义,init函数和main函数,则执行的流程全局变量定义->init函数->main函数
- init函数最主要的作用,就是完成一些初始化的工作
- 如果main.go和utils.go都含有变量定义,init函数时,执行的流程又是怎么样的呢?
main.go
import ".../utils"
变量定义 //3
init 函数 //4
main 函数 //5
//***********
utils.go
变量定义 //1
init函数 //2
匿名函数
Go支持匿名函数,匿名函数就是没有名字的函数,如果我们某个函数只是希望使用一次,可以考虑使用匿名函数,匿名函数也可以实现多次调用。
使用
方式一:
在定义匿名函数时就直接调用,这种方式匿名函数只能调用一次
func main(){
res1:=func(n1 int,n2 int)int{
return n1+n2
}(10,20)
fmt.Println("res1=",res1)
}
方式二:
将匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数
a:=func(n1 int,n2 int)int{
return n1-n2
}
res2:=a(10,30)
fmt.Println("res2=",res2)
res3:=a(20,60)
fmt.Println("res3=",res3)
全局匿名函数
如果将匿名函数赋给一个全局变量,那么这个匿名函数,就成为一个全局匿名函数,可以在程序有效。
var(
Fun1=func(n1 int,n2 int)int{
return n1*n2
}
)
res4:=Fun1(4,9)
fmt.Println("res4=",res4)
闭包
闭包就是一个函数和与其相关的引用环境组合的一个整体(实体)
案例
func addupper() func (int) int{
var a =10
return func(n int) int {
a=a+n
return a
}
}
func main(){
f:=addupper()
fmt.Println(f(1))
fmt.Println(f(3))
fmt.Println(f(5))
}
-
addupper是一个函数,返回的数据类型是fun (int) int
-
闭包的说明:
返回的是一个匿名函数,但是这个匿名函数引用到函数外的n,因此这个匿名函数就和n形成一个整体,构成闭包。
-
可以这样理解:闭包是类,函数是操作,n是字段。函数和它使用到n构成闭包
-
当我们反复的调用f函数时,因为n是初始化一次,因此每调用一次就进行累计
-
我们要搞清楚闭包的关键,就是要分析出返回的函数它使用(引用)到哪些变量,因为函数和它引用到的变量共同构成闭包
实践
- 编写一个函数makeSuffix(suffixstring)可以接收一个文件后缀名(比如.jpg),并返回一个闭包
- 调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀(比如.jpg),则返回文件名.jpg,如果已经有.jpg后缀,则返回原文件名
- 要求使用闭包的方式完成
- strings.HasSuffix,该函数可以判断某个字符串是否有指定的后缀
func makesuffix(suffix string) func (string) string{
return func(name string) string {
if !strings.HasSuffix(name,suffix){
return name+suffix
}
return name
}
}
func main(){
f:=makesuffix(".jpg")
fmt.Println(f("winter")) //winter.jpg
fmt.Println(f("spring.jpg")) //spring.jpg
}
上面程序说明
- 返回的匿名函数和makeSuffix(suffixstring)的suffix变量组合成一个闭包,因为返回的函数引用到suffix这个变量
- 我们体会一下闭包的好处,如果使用传统的方法,也可以轻松实现这个功能,但是传统方法需要每次都传入后缀名,比如.jpg,而闭包因为可以保留上次引用的某个值,所以我们传入一次就可以反复使用
函数的defer
在函数中,程序员经常需要创建资源(比如:数据库连接、文件句柄、锁等),为了在函数执行完毕后,及时的释放资源,Go的设计者提供defer(延时机制)
func sum(n int,m int){
defer fmt.Println("----",n)
defer fmt.Println("****",m)
fmt.Println("和为:",n+m)
}
func main(){
sum(1,2)
}
//结果↓
/*
和为: 3
**** 2
---- 1
*/
细节
- 当go执行到一个defer时,不会立即执行defer后的语句,而是将defer后的语句压入到一个栈中[暂时称该栈为defer栈],然后继续执行函数下一个语句
- 当函数执行完毕后,在从defer栈中,依次从栈顶取出语句执行(注:遵守栈先入后出的机制)
- 在defer将语句放入到栈时,也会将相关的值拷贝同时入栈
func sum(n int,m int){
defer fmt.Println("----",n)
defer fmt.Println("****",m)
n++
m++
fmt.Println("和为:",n+m)
}
func main(){
sum(1,2)
}
//结果
/*
和为: 5
**** 2
---- 1
*/
最佳实践
defer最主要的价值是在,当函数执行完毕后,可以及时的释放函数创建的资源
模拟代码:
func test(){
//关闭文件资源
file=openfile(文件名)
defer file.close
//其他代码
}
//***************************
fun test(){
//释放数据库资源
connect=openDatabase()
defer connect.close()
//其他代码
}
- 在golang编程中的通常做法是,创建资源后,比如(打开了文件,获取了数据库的链接,或者是锁资源),可以执行deferfile.Close()deferconnect.Close()
- 在defer后,可以继续使用创建资源
- 当函数完毕后,系统会依次从defer栈中,取出语句,关闭资源
- 这种机制,非常简洁,程序员不用再为在什么时机关闭资源而烦心
函数的参数传递方式
我们在讲解函数注意事项和使用细节时,已经讲过值类型和引用类型了,这里我们再系统总结一下,因为这是重难点,值类型参数默认就是值传递,而引用类型参数默认就是引用传递
两种传递方式
- 值传递
- 引用传递
其实,不管是值传递还是引用传递,传递给函数的都是变量的副本,不同的是,值传递的是值的拷贝,引用传递的是地址的拷贝,一般来说,地址拷贝效率高,因为数据量小,而值拷贝决定拷贝的数据大小,数据越大,效率越低
值类型和引用类型
- 值类型:基本数据类型int系列,float系列,bool,string、数组和结构体struct
- 引用类型:指针、slice切片、map、管道chan、interface等都是引用类型
值类型和引用类型使用特点
- 值类型默认是值传递:变量直接存储值,内存通常在栈中分配
- 引用类型默认是引用传递:变量存储的是一个地址,这个地址对应的空间才真正存储数据,内存通常在堆上分配,当没有任何变量引用这个地址时,该地址对应的数据空间就成为一个垃圾,由GC来回收
- 如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用
变量作用域
- 函数内部声明/定义的变量叫局部变量,作用域仅限于函数内部
- 函数外部声明/定义的变量叫全局变量,作用域在整个包都有效,如果其首字母为大写,则作用域在整个程序有效
- 如果变量是在一个代码块,比如for/if中,那么这个变量的的作用域就在该代码块
字符串常用系统函数
- 统计字符串的长度,按字节len(str)
- 字符串遍历,同时处理有中文的问题r:=[]rune(str)
var str string="hello world,北京"
str2:= []rune(str)
for i:=0;i<len(str2);i++{
fmt.Printf("%c ",str2[i])
}
3. 字符串转整数:n,err:=strconv.Atoi("12")
n,err:=strconv.Atoi("hello")
if err!=nil{
fmt.Println("转换错误",err)
}else{
fmt.Println("转成的结果是",n)
}
-
整数转字符串str=strconv.Itoa(12345)
-
字符串转[]byte: var bytes = []byte(“hello go”)
-
[]byte转字符串:str=string([]byte{97,98,99})
-
10进制转2,8,16进制:str=strconv.FormatInt(123,2)//2->8,16
-
查找子串是否在指定的字符中:strings.Contains(“seafood”,“foo”)//true
-
统计一个字符串有几个指定的子串:strings.Count(“ceheese”,“e”)//4
-
不区分大小写的字符串比较(==是区分字母大小写的):fmt.Println(strings.EqualFold(“abc”,“Abc”))//true
-
返回子串在字符串第一次出现的index值,如果没有返回-1:strings.Index(“NLT_abc”,“abc”)//4
-
返回子串在字符串最后一次出现的index,如没有返回-1:strings.LastIndex(“gogolang”,“go”)
-
将指定的子串替换成另外一个子串:strings.Replace(“gogohello”,“go”,“go语言”,n)n可以指定你希望替换几个,如果n=-1表示全部替换
-
按照指定的某个字符,为分割标识,将一个字符串拆分成字符串数组:strings.Split(“hello,wrold,ok”,",")
-
将字符串的字母进行大小写的转换:
strings.ToLower(“Go”)//go
strings.ToUpper(“Go”)//GO
-
将字符串左右两边的空格去掉:strings.TrimSpace(“tnalonegopherntrn”)
-
将字符串左右两边指定的字符去掉:strings.Trim("!hello!","!")//[“hello”]//将左右两边!和""去掉
-
将字符串左边指定的字符去掉:strings.TrimLeft("!hello!","!")//[“hello”]//将左边!和""去掉
-
将字符串右边指定的字符去掉:strings.TrimRight("!hello!","!")//[“hello”]//将右边!和""去掉
-
判断字符串是否以指定的字符串开头:strings.HasPrefix(“ftp://192.168.10.1”,“ftp”)//true
-
判断字符串是否以指定的字符串结束:strings.HasSuffix(“NLT_abc.jpg”,“abc”)//false
时间和日期相关函数
- 时间和日期相关函数,需要导入time包
- time.Time类型,用于表示时间
now:=time.Now()
fmt.Printf("now :%v type: %T",now,now)
3. 如何获取到其它的日期信息
fmt.Printf("年=%v\n",now.Year())
fmt.Printf("月=%v\n",now.Month()) //September
fmt.Printf("月=%v\n",int(now.Month())) //9
fmt.Printf("日=%v\n",now.Day())
fmt.Printf("时=%v\n",now.Hour())
fmt.Printf("分=%v\n",now.Minute())
fmt.Printf("秒=%v\n",now.Second())
-
格式化日期时间
方式1:就是使用Printf或者SPrintf
fmt.Printf("当前年月日=%d-%d-%d %d:%d:%d\n",now.Year(),now.Month(),now.Day(),now.Hour(),now.Minute(),now.Second()) dateStr:=fmt.Sprintf("当前年月日=%d-%d-%d %d:%d:%d\n",now.Year(),now.Month(),now.Day(),now.Hour(),now.Minute(),now.Second()) fmt.Printf("当前年月日=%v",dateStr)
方式二:使用time.Format()方法完成
fmt.Println(now.Format("2006-01-02 15:04:05")) //2021-09-12 22:08:58 fmt.Println(now.Format("2006-01-02")) //2021-09-12 fmt.Println(now.Format("15:04:05")) //22:08:58
对上面代码的说明:
-
"2006/01/0215:04:05"这个字符串的各个数字是固定的,必须是这样写。
-
"2006/01/0215:04:05"这个字符串各个数字可以自由的组合,这样可以按程序需求来返回时间和日期
- 时间的常量
-
const(
NanosecondDuration=1//纳秒 Microsecond=1000*Nanosecond//微秒 Millisecond=1000*Microsecond//毫秒Second=1000*Millisecond//秒Minute=60*Second//分钟Hour=60*Minute//小时
)
常量的作用:
-
在程序中可用于获取指定时间单位的时间,比如想得到100毫秒100*time.Millisecond
-
结合Sleep来使用一下时间常量
time.Sleep(time.Millisecond * 100)
-
time的Unix和UnixNano的方法
-
Unix将t表示为Unix时间,即从时间点January 1, 1970 UTC到时间点t所经过的时间(单位秒)。
在windows下,rand.Seed(time.Now().Unix())作为种子,得出的随机数是随机的 -
UnixNano将t表示为Unix时间,即从时间点January 1, 1970 UTC到时间点t所经过的时间(单位纳秒)。如果纳秒为单位的unix时间超出了int64能表示的范围,结果是未定义的。注意这就意味着Time零值调用UnixNano方法的话,结果是未定义的。
在windows下,rand.Seed(time.Now().UnixNano())作为种子,得出的随机数并不随机
-
-
一段程序的执行时间
-
start:=time.Now().Unix()
test()
end:=time.Now().Unix()
fmt.Printf("执行test函数耗费时间为%v秒\n",end-start)
内置函数
Golang设计者为了编程方便,提供了一些函数,这些函数可以直接使用,我们称为Go的内置函数。文档:https://studygolang.com/pkgdoc->builtin
- len:用来求长度,比如string、array、slice、map、channel
- new:用来分配内存,主要用来分配值类型,比如int、float32,struct…返回的是指针
num:=new(int)
//num是*int型 num是一个地址
3. make:用来分配内存,主要用来分配引用类型,比如channel、map、slice
错误处理
- 在默认情况下,当发生错误后(panic),程序就会退出(崩溃.)
- 如果我们希望:当发生错误后,可以捕获到错误,并进行处理,保证程序可以继续执行。还可以在捕获到错误后,给管理员一个提示(邮件,短信。。。)
- 这里引出我们要将的错误处理机制
说明
- Go语言追求简洁优雅,所以,Go语言不支持传统的try…catch…finally这种处理。
- Go中引入的处理方式为:defer,panic,recover
- 这几个异常的使用场景可以这么简单描述:Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理
func test(){
defer func() {
//recover内置函数,可以捕获异常
err:=recover()
if err!=nil{ //说明捕获到错误
fmt.Println("错误为:",err)
}
}()
num1:=10
num2:=0
res:=num1/num2
fmt.Println("res=",res)
}
func main(){
test()
}
//错误为: runtime error: integer divide by zero
好处
进行错误处理后,程序不会轻易挂掉,如果加入预警代码,就可以让程序更加的健壮
自定义错误
Go程序中,也支持自定义错误,使用errors.New和panic内置函数
- errors.New(“错误说明”),会返回一个error类型的值,表示一个错误
- panic内置函数,接收一个interface{}类型的值(也就是任何值了)作为参数。可以接收error类型的变量,输出错误信息,并退出程序
案例:
func readConf(name string)(err error){
if name=="config.ini"{
return nil
}else {
return errors.New("文件读取有误")
}
}
func test(){
err:=readConf("conf")
if err!=nil{
panic(err)
}
}
func main(){
test()
}
数组
定义
var数组名 [数组大小] 数据类型
var a [5] int
赋初值 a[0]=1 a[1]=30…
var arr [10] float64
arr[0]=1
arr[1]=2
..
- 数组的地址可以通过数组名来获取&arr
- 数组的第一个元素的地址,就是数组的首地址
- 数组的各个元素的地址间隔是依据数组的类型决定,比如int64->8int32->4
初始化数组
var numArr01 [3]int = [3]int{1,2,3}
var numArr02 = [3]int{5,6,7}
var numArr03=[...]int{8,9,10} //[...]是规定写法
var numArr04=[...]int{1:800,0:900,2:999}
strarr05:=[...]string{1:"tom",0:"jack",2:"mary"}
数组遍历
- 普通for循环遍历
- for-range遍历
for index,value:=range array01{
...
}
/*
index是数组下标,如果不想下标使用就用_
value是该下标位置的值
他们都是for循环内部可见的局部变量
index和value不是固定名称,可以自己定义
*/
heroes :=[...]string{"宋江","吴用","卢俊义"}
for i,v:=range heroes{
fmt.Printf("i=%v v=%v\n",i,v)
fmt.Printf("heroes[%d]=%v",o,heroes[i])
}
for _,v:=range heroes{
fmt.Printf("元素的值为:%v",v)
}
注意事项
- 数组是多个相同类型数据的组合,一个数组一旦声明/定义了,其长度是固定的,不能动态变化
- var arr[]int这时arr就是一个slice切片,切片后面专门讲解
- 数组中的元素可以是任何数据类型,包括值类型和引用类型,但是不能混用
- 数组创建后,如果没有赋值,有默认值(零值)
- 使用数组的步骤1.声明数组并开辟空间2给数组各个元素赋值(默认零值)3使用数组
- 数组的下标是从0开始的
- 数组下标必须在指定范围内使用,否则报panic:数组越界,比如var arr[5]int则有效下标为0-4
- Go的数组属值类型,在默认情况下是值传递,因此会进行值拷贝。数组间不会相互影响
- 如想在其它函数中,去修改原来的数组,可以使用引用传递(指针方式)
func test(arr *[3]int){
(*arr)[0]=新值
}
10. 长度是数组类型的一部分,在传递函数参数时需要考虑数组的长度
案例
创建一个byte类型的26个元素的数组,分别放置’A’-‘Z‘。使用for循环访问所有元素并打印出来。提示:字符数据运算’A’+1->‘B’
var mychars [26]byte
for i:=0;i<26;i++{
mychars[i]='A'+byte(i) //注意将i转成int型
}
二维数组
语法: var 数组名 [大小] [大小] 类型
比如: var arr [2] [3] int,再赋值
内存
二维数组内存,看有几行,每行有一块连续的内存
初始化数组
声明:var数组名[大小] [大小]类型=[大小] [大小]类型{{初值…},{初值…}}
赋值 (有默认值,比如int类型的就是0)
var arr[2][3]=[2][3]int{{0,1,2},{3,4,5}}
说明:二维数组在声明/定义时也对应有四种写法[和一维数组类似]
var数组名[大小] [大小]类型=[大小] [大小]类型{{初值…},{初值…}}
var数组名[大小] [大小]类型=[…] [大小]类型{{初值…},{初值…}}
var数组名=[大小] [大小]类型{{初值…},{初值…}}
var数组名=[…] [大小]类型{{初值…},{初值…}}
遍历
方式一:
普通for循环
方式二:
for-range
for i,v1:=range arr{
for j,v2:range v1{
fmt.Printf("arr[%v][%v]=%v",i,j,v2)
}
}
切片
-
切片的英文是slice
-
切片是数组的一个引用,因此切片是引用类型,在进行传递时,遵守引用传递的机制
-
切片的使用和数组类似,遍历切片、访问切片的元素和求切片长度len(slice)都一样
-
切片的长度是可以变化的,因此切片是一个可以动态变化数组
-
切片定义的基本语法:
var 切片名 [] 类型 比如:var a []int
func main(){
var numArr04 = [...]int{0:800,1:900,2:999,3:555}
slice:=numArr04[1:3] //下标包括1 但不包括3
fmt.Println("slice=",slice)
fmt.Println("slice=",len(slice)) //2 长度
fmt.Println("slice=",cap(slice)) //3 切片容量
}
cap解释
array = []int{1,2,3,4,5,6,7,8,9}
// 从array取,左指针索引为0,右指针为5,切片是从array切的,
// 而且cap函数只计算左指针到原array最后的值的个数
slice = array[0:5] // slice ==> {1, 2, 3, 4, 5}
cap(slice) // == 9,因为左指针索引为0,到结尾有9个数,cap为9
slice = slice[2:] // slice ==> {3, 4, 5}
cap(slice) // == 7 左指针偏移了2步,所以cap为9-2=7
内存
切片中内存存储是三块,第一块是数组地址,地址指向的位置存储着数组元素,第二块是长度len,第三块是容量cap
-
slice的确是一个引用类型
-
slice从底层来说,其实就是一个数据结构(struct结构体)
type slice struct{
ptr *[2] int
len int
cap int
}
使用
方式一:
定义一个切片,然后让切片去引用一个已经创建好的数组,比如前面的案例就是这样的
方式二:
通过make来创建切片
基本语法:var 切片名 [] type = make([]type,len,[cap])
参数说明: type:就是数据类型 len:大小 cap:指定切片容量,可选,如果你分配了cap, 则要求cap>=len
var slice[]float64=make([]float64,5,10)
slice[1]=10
slice[3]=10
- 通过make方式创建切片可以指定切片的大小和容量
- 如果没有给切片的各个元素赋值,那么就会使用默认值[int,float=>0 string=>”” bool=>false]
- 通过make方式创建的切片对应的数组是由make底层维护,对外不可见,即只能通过slice去访问各个元素.
方式三:
定义一个切片,直接就指定具体数组,使用原理类似make的方式
var strSlice[]string=[]string{"tom","jack","mary"}
遍历
切片的遍历和数组一样,也有两种方式
方式一:
普通for循环遍历
方式二:
for-range
细节
-
切片初始化时var slice=arr[startIndex:endIndex]
说明:从arr数组下标为startIndex,取到下标为endIndex的元素(不含arr[endIndex])
-
切片初始化时,仍然不能越界。范围在[0-len(arr)]之间,但是可以动态增长
var slice=arr[0:end] 可以简写 var slice=arr[:end]
var slice=arr[start:len(arr)] 可以简写:var slice=arr[start:]
var slice=arr[0:len(arr)] 可以简写:var slice=arr[:]
-
cap是一个内置函数,用于统计切片的容量,即最大可以存放多少个元素
-
切片定义完后,还不能使用,因为本身是一个空的,需要让其引用到一个数组,或者make一个空间供切片来使用
-
切片可以继续切片
-
用append内置函数,可以对切片进行动态追加
var slice[]int=[]int{100,200,300}
slice=append(slice,400,500) //追加
slice=append(slice,slice) //把自己已有的追加到后面
切片append操作的底层原理分析:
切片append操作的本质就是对数组扩容go底层会创建一下新的数组newArr(安装扩容后大小)将slice原来包含的元素拷贝到新的数组newArrslice重新引用到newArr注意newArr是在底层来维护的,程序员不可见
7. 切片的拷贝操作切片使用copy内置函数完成拷贝
copy(slice1,slice2) //把切片2复制到切片1上
-
copy(para1,para2)参数的数据类型是切片
-
slice1和slice2的数据空间是独立,相互不影响
- 切片是引用类型,所以在传递时,遵守引用传递机制
var numArr04 = [...]int{800,900,999,555}
slice:=numArr04[:]
fmt.Println("slice=",slice)//[800 900 999 555]
test(slice)
fmt.Println("slice=",slice)//[100 900 999 555]
9. 下面代码没错,输出800
var numArr04 = []int{800,900,999,555}
var slice=make([]int,1)
copy(slice,numArr04)
fmt.Println("slice=",slice) //800
string和slice
- string底层是一个byte数组,因此string也可以进行切片处理
- string在内存中有两块,第一块是一个地址指向数组元素,第二块是长度
- string是不可变的,也就说不能通过str[0]='z’方式来修改字符串
- 如果需要修改字符串,可以先将string->[]byte/或者[]rune->修改->重写转成string
map
map是key-value数据结构,又称为字段或者关联数组。类似其它编程语言的集合,在编程中是经常使用到
语法
var map变量名 map[keytype] valuetype
类型
- key可以是什么类型?
golang中的map,的key可以是很多种类型,比如bool,数字,string,指针,channel,还可以是只包含前面几个类型的接口,结构体,数组通常key为int、string注意:slice,map还有function不可以,因为这几个没法用==来判断
2. valuetype可以是什么类型
valuetype的类型和key基本一样,这里我就不再赘述了通常为:数字(整数,浮点数),string,map,struct
举例
var a map[string]string
var a map[string]int
var a map[int]string
var a map[string]map[string]string
声明是不会分配内存的,初始化需要make,分配内存后才能赋值和使用
var a map[string]string
a=make(map[string]string,10)
a["first"]="张三"
a["second"]="李四"
a["third"]="王五"
fmt.Println("a=",a)
fmt.Println("first=",a["first"])
//a= map[first:张三 second:李四 third:王五]
//first= 张三
说明:
- map在使用前一定要make
- map的key是不能重复,如果重复了,则以最后这个key-value为准
- map的value是可以相同的.
- map的key-value是无序
- make内置函数数目
使用
方式一:
前面的例子
方式二:
前面的例子直接 a:=make(map[string]string,10)
方式三:
a := map[string]string{
"first":"张三",
"second":"李四",
"third":"王五",
}
a["fuoth"]="刘六"
fmt.Println("a=",a)
//a= map[first:张三 fuoth:刘六 second:李四 third:王五]
fmt.Println("first=",a["first"]) //first= 张三
小案例:
演示一个key-value的value是map的案例比如:我们要存放学生信息,每个学生有name、sex和address信息
studentMap := make(map[string]map[string]string)
studentMap["stu01"]=make(map[string]string)
studentMap["stu01"]["name"]="tom"
studentMap["stu01"]["sex"]="男"
studentMap["stu01"]["address"]="山东"
studentMap["stu02"]=make(map[string]string)
studentMap["stu02"]["name"]="mary"
studentMap["stu02"]["sex"]="女"
studentMap["stu02"]["address"]="北京"
fmt.Println("stu01=",studentMap["stu01"])
fmt.Println("stu02=",studentMap["stu02"])
fmt.Println("stu02 sex=",studentMap["stu02"]["sex"])
//stu01= map[address:山东 name:tom sex:男]
//stu02= map[address:北京 name:mary sex:女]
//stu02 sex= 女
map增删改查
-
map增加和更新:map[“key”]=value
//如果key还没有,就是增加,如果key存在就是修改。
-
map删除:说明:delete(map,“key”),delete是一个内置函数,如果key存在,就删除该key-value,如果key不存在,不操作,但是也不会报错
注意
如果我们要删除map的所有key,没有一个专门的方法一次删除,可以遍历一下key,逐个删除或者map=make(…),make一个新的,让原来的成为垃圾,被gc回收
-
map查找:
val,ok:=a["first"]
if ok{
fmt.Println("有first key值为%v",val)
}else{
fmt.Println("没有first key")
}
说明:如果a这个map中存在"first",那么find Res就会返回true,否则返回false
map遍历
上面的例子
studentMap := make(map[string]map[string]string)
studentMap["stu01"]=make(map[string]string)
studentMap["stu01"]["name"]="tom"
studentMap["stu01"]["sex"]="男"
studentMap["stu01"]["address"]="山东"
studentMap["stu02"]=make(map[string]string)
studentMap["stu02"]["name"]="mary"
studentMap["stu02"]["sex"]="女"
studentMap["stu02"]["address"]="北京"
for i,v1:=range studentMap{
fmt.Println(i)
for j,v2:=range v1{
fmt.Println(j,v2)
}
}
map切片
切片的数据类型如果是map,则我们称为sliceofmap,map切片,这样使用则map个数就可以动态变化了
案例
要求:使用一个map来记录monster的信息name和age,也就是说一个monster对应一个map,并且妖怪的个数可以动态的增加=>map切片
var monsters []map[string]string
monsters=make([]map[string]string,2)
if monsters[0]==nil{
monsters[0]=make(map[string]string,2)
monsters[0]["name"]="aaaa"
monsters[0]["age"]="10"
}
if monsters[1]==nil{
monsters[1]=make(map[string]string,2)
monsters[1]["name"]="bbbb"
monsters[1]["age"]="20"
}
fmt.Println(monsters[0]) //map[age:10 name:aaaa]
fmt.Println(monsters[1]) //map[age:20 name:bbbb]
fmt.Println(monsters[0]["age"]) //10
//先创建一个新妖怪 再append
newmonster:=map[string]string{
"name":"cccc",
"age":"30",
}
monsters=append(monsters,newmonster)
fmt.Println(monsters[2]) //map[age:20 name:bbbb]
map排序
- golang中没有一个专门的方法针对map的key进行排序
- golang中的map默认是无序的,注意也不是按照添加的顺序存放的,你每次遍历,得到的输出可能不一样
- golang中map的排序,是先将key进行排序,然后根据key值遍历输出即可
map1:=make(map[int]int,5)
map1[0]=1
map1[2]=3
map1[1]=5
map1[4]=10
map1[3]=20
fmt.Println(map1) //map[0:1 1:5 2:3 3:20 4:10]
//先将map的key放入keys中 然后排序key 最后遍历key根据map1[key]得到值
var keys[]int
for i,_:=range map1{
keys=append(keys,i)
}
fmt.Println(keys) //[3 0 2 1 4]
sort.Ints(keys)
fmt.Println(keys) //[0 1 2 3 4]
for _,k:=range keys{
fmt.Printf("map[%v]=%v ",k,map1[k])
//map[0]=1 map[1]=5 map[2]=3 map[3]=20 map[4]=10
}
细节
- map是引用类型,遵守引用类型传递的机制,在一个函数接收map,修改后,会直接修改原来的map
- map的容量达到后,再想map增加元素,会自动扩容,并不会发生panic,也就是说map能动态的增长键值对(key-value)
- map的value也经常使用struct类型,更适合管理复杂的数据(比前面value是一个map更好),比如value为Student结构体
面向对象编程
- Golang也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说Golang支持面向对象编程特性是比较准确的
- Golang没有类(class),Go语言的结构体(struct)和其它编程语言的类(class)有同等的地位,你可以理解Golang是基于struct来实现OOP特性的
- Golang面向对象编程非常简洁,去掉了传统OOP语言的继承、方法重载、构造函数和析构函数、隐藏的this指针等等
- Golang仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它OOP语言不一样,比如继承:Golang没有extends关键字,继承是通过匿名字段来实现
- Golang面向对象(OOP)很优雅,OOP本身就是语言类型系统(typesystem)的一部分,通过接口(interface)关联,耦合性低,也非常灵活。后面会充分体会到这个特点。也就是说在Golang中面向接口编程是非常重要的特性
结构体
入门
type Cat struct {
name string
age int
color string
}
func main(){
var cat1 Cat
cat1.name="喵喵"
cat1.age=12
cat1.color="红色"
var cat2 Cat
cat2.name="虎虎"
cat2.age=20
cat2.color="蓝色"
fmt.Println(cat1) //{喵喵 12 红色}
fmt.Println(cat2) //{虎虎 20 蓝色}
}
字段/属性
-
从概念或叫法上看:结构体字段=属性=field(即授课中,统一叫字段)
-
字段是结构体的一个组成部分,一般是基本数据类型、数组,也可是引用类型。比如我们前面定义猫结构体的name string就是属性
-
字段声明语法同变量,示例:字段名字段类型
-
字段的类型可以为:基本类型、数组或引用类型
-
在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值)
指针,slice,和map的零值都是nil,即还没有分配空间
-
不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个,结构体是值类型
创建结构体变量和访问结构体字段
方式一:
直接声明 前面的例子
方式二:
cat3:=Cat{"咪咪",25,"紫色"}
fmt.Println(cat3) //{咪咪 25 紫色}
方式三:
var cat4 *Cat=new(Cat)
//因为cat4是一个指针,所以需要解引用,不过go创作者为了方便,会在底层进行操作,所以解引用不解引用效果一样
(*cat4).age=30
cat4.color="蓝色"
(*cat4).name="哗哗"
fmt.Println(*cat4) //{哗哗 30 蓝色}
方式四:
var cat5 *Cat=&Cat{}
//和方式三同理
cat5.name="西西"
cat5.age=35
(*cat5).color="黄色"
fmt.Println(*cat5) //{西西 35 黄色}
说明:
- 第3种和第4种方式返回的是结构体指针
- 结构体指针访问字段的标准方式应该是:(*结构体指针).字段名
- 但go做了一个简化,也支持结构体指针.字段名,比如person.Name=“tom”。更加符合程序员使用的习惯,go编译器底层对person.Name做了转化(*person).Name
- . 的运算优先级比*高
细节
- 结构体的所有字段在内存中是连续的
- 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
- 结构体进行type重新定义(相当于取别名),Golang认为是新的数据类型,但是相互间可以强转
- struct的每个字段上,可以写上一个tag,该tag可以通过反射机制获取,常见的使用场景就是序列化和反序列化(反射部分介绍)
方法
在某些情况下,我们要需要声明(定义)方法。比如Cat结构体:除了有一些字段外(年龄,姓名…),Person结构体还有一些行为比如:可以说话、跑步…,通过学习,还可以做算术题。这时就要用方法才能完成
Golang中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct
声明和调用
func (c Cat) test(){
fmt.Println("test...",c.name)
}
cat5.test() //test... 西西
cat1.test() //test... 喵喵
- func(a A)test(){}表示A结构体有一方法,方法名为test
- (a A)体现test方法是和A类型绑定的
- test方法和Cat类型绑定
- test方法只能通过Cat类型的变量来调用,而不能直接调用,也不能使用其它类型变量来调用
- func(c Cat) test() {}…p表示哪个Cat变量调用,这个c就是它的副本,这点和函数传参非常相似
- c这个名字,有程序员指定,不是固定,比如修改成cat也是可以
调用和传参机制
方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将调用方法的变量,当做实参也传递给方法
说明
- 在通过一个变量去调用方法时,其调用机制和函数一样
- 不一样的地方时,变量调用方法时,该变量本身也会作为一个参数传递到方法(如果变量是值类型,则进行值拷贝,如果变量是引用类型,则进行地质拷贝)
方法的声明(定义)
func(recevier type) methodName(参数列表)(返回值列表){
方法体
return返回值
}
- 参数列表:表示方法输入
- receviertype:表示这个方法和type这个类型进行绑定,或者说该方法作用于type类型
- receivertype:type可以是结构体,也可以其它的自定义类型
- receiver:就是type类型的一个变量(实例),比如:Person结构体的一个变量(实例)
- 返回值列表:表示返回的值,可以多个
- 方法主体:表示为了实现某一功能代码块
- return语句不是必须的
细节
- 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
- 如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
- Golang中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct,比如int,float32等都可以有方法
- 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问
- 如果一个类型实现了String()这个方法,那么fmt.Println默认会调用这个变量的String()进行输出
方法和函数区别
- 调用方式不一样
- 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
- 对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以
- 不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定.
- 如果是和值类型,比如(p Person),则是值拷贝,如果和指针类型,比如是(p*Person)则是地址拷贝