为什么golang map slicee可以直接定义使用,而map不可以直接定义使用

&&国之画&&&& &&&&&&
&& &&&&&&&&&&&&&&&&&&&&
鲁ICP备号-4
打开技术之扣,分享程序人生!编程语言 – e弧度
赞助商广告
首先入门推荐开源图书Go Web编程,
这本书也已经出版,如果喜欢纸质书,可以购买。
当时golang的中文资料还比较少,我最先接触到的是The way to go的译本。
网上早期流传的几本关于go的书我都买了,比如Go语言云动力,是一个新加坡的人出的,当时看作者介绍说是一个黑客然后我就买了!应该是出版最早的一本关于Go的中文图书。
后面有许式伟出的Go语言编程,据说是国内最早运用Go语言七牛公司的老总,所以也买了。
感觉还不够,后面出来了一本Go语言程序设计(21世纪高等学校规划教材 计算机科学与技术),哈哈大学教材,估计比较全面,然后也买了。
对于编程没啥基础的我认为go web编程与Go语言程序设计两本书还是值得推荐的。The way to go译文也还可以,就是针对的版本有点老了。另外两本就不怎么推荐了,打击初学者的学习信心。
关于本文:
本文讲述Go编程学习过程,以及使用Go编程解决平常学习、工作中所碰到的问题,提高用Go语言解决问题的能力以及怎么应用到实际的项目中,以达到学以致用的目的。
先对男女主角简单介绍一下吧。
这是高富帅:
这是白富美:
最近高富帅接触了电脑编程,有点入迷,认为用编程捣鬼一些程序实现一些功能是很有趣的事情。所以高富帅打算学习一门编程语言,做一些有趣的玩意也好在别人面前炫耀炫耀。但是编程语言非常多,该学习什么样的编程语言达到自己的目的呢?于是乎高富帅动用了Google(搜索引擎,几乎可以搜索到你想要的内容网址:),经过数据挖掘可是花了高富帅好长时间,因为他知道成功的第一步很重要,最后发现Go语言非常适合自己,最终就确定了开始学习Go语言的计划来。
他选择编程语言的标准是什么?Go语言有什么特别之处呢?高富帅为什么选择Go语言呢?
高富帅考虑这点很简单,因为每个人使用的电脑、操作系统可能都不一样,如果不能保证在不同机器上运行那就达不到编写一次在别人机器运行的目的,那就不能炫耀给别人看啦,之前看到python、java之类的宣称自己跨平台,最后发现被忽悠了。原来它们宣称的跨平台是必须要安装它们的软件,一个叫什么虚拟机或解析器之类的运行环境,代码才能运行,高富帅果断pass掉。你总不能传过文件过去叫妹纸运行之前教她下载安装个虚拟机吧!Go就可以一次编写跨平台运行,目前支持的操作系统包括Linux、Mac OS X、FreeBSD、Windows(Go1.3.1数据),支持交叉编译,也就是说如果我用的是Windows系统编写的软件,如果妹纸用的是苹果的Mac OS X系统,我可以在Windows系统生成一个能在她系统上运行的软件传给她运行就可以在她电脑上看到效果,是不是很酷啊。软件一点击运行即可看到运行结果,在高富帅的眼里这才是真正的跨平台。 现在的Android是使用Java开发的,Go团队在Go1.4版本中提议支持Android,不过是通过NDK来实现的,现在Java被甲骨文收购以及Android与甲骨文的官司,有理由相信,在将来时机成熟,Android会是以Go语言为官方的开发语言。
高富帅知道自己不算聪明但也不是傻子,如果很难学习那估计会失去学习的兴趣。高富帅发现Go语言只有25个关键字要记忆,而且学习了一遍Go语言编程之旅简单的示例,发现还算易学,而且也有中文资料,在中国这门语言还是比较多人关注的。
这是官方介绍所得知的,由于是初学者也就相信吧,毕竟其它开发者反映Go语言的性能还是比较好的。由于Go语言是静态编程语言,程序必须经过编译才能运行,编译也是非常的快的。
支持多核心架构
支持多核,自动利用多核计算自然运行速度就快了,由于Go语言还很年轻,似乎还没有真正实现,相信大牛们在不远的将来就会实现的。
Go语言从语言层面直接支持高并发,编写高并发的网络服务程序变得更简单。
Go语言采用BSD开源协议授权,你可以使用Go语言编写程序,也可以对Go语言进行改造,你对Go语言进行修改可以选择公开源代码也可以不公开,你可以用Go语言进行商业活动而不需要Go语言官方任何授权。
牛人设计的语言
由贝尔实验室包括肯·汤普森(Ken Thompson)在内的Plan 9原班团队开发的。
发展前景良好
Google大公司支持、Android的流行以及Go1.4开始考虑支持Android、很多大公司开始用Go语言开发应用,如淘宝、360、新浪、京东、土豆
(一)下载
Go语言官方网址: 是个英文网站,要一点点英文基础,很容易就找到下载链接。不过由于防火墙的存在,你将无法访问该网站!你可以使用代理软件翻墙,怎么翻?请自行Google或百度,翻墙不在本文介绍范围。读者请根据自己运行的系统下载相应的软件进行安装。特别要注意你的操作系统是64位还是32位的。Windows XP一般为32位,Win7右击桌面计算机,选择属性查看我的电脑里面的系统类型会显示是32位还是64位系统。如果确实不清楚自己的操作系统是多少位的,可以先下载Go语言64位的安装程序进行安装,如果是64位操作系统能正常安装,32位则不能正常安装,这时才下载32位安装文件进行安装。Go语言目前最新版本为1.3.1,32位的安装文件一般有386的字样,64位的安装文件一般带有amd64字样。如go1.3.1.windows-386.msi是windows32位安装文件,go1.3.1.windows-amd64.msi是windows64位安装文件。
当然,如果确实找不到安装文件下载,这里提供windows系统64位与32位安装文件
(二)安装
Go最常用的安装方法有:源码安装、标准包安装和第三方工具安装,可以选择适合自己的方法进行安装。
标准包的安装:
以32位windows系统为例,64位windows进行安装类似,安装很简单只要鼠标点点基本就是下一步就OK了。
1、双击运行下载的文件,会出现如图1-1所示安装欢迎界面,单击Next按钮,进入用户许可协议界面。
图1-1 Go安装欢迎界面
2、在用户协议许可界面中,勾选I accept the terms in the License Agreement复选框(图1-2),单击Next按钮,进入设置安装路径对话框。
图1-2 用户许可协议界面
3、在设置安装路径对话框中,默认安装路径是C:\Go\,如图1-3所示。如果需要更改,在路径输入框中输入新的路径信息,也可以单击Change按钮进行浏览磁盘路径进行选择,然后单击Next按钮;否则直接单击Next按钮,进入准备安装界面。
图1-3 设置安装路径对话框
4、进入准备安装界面后,直接单击Install按钮(图1-4),进入安装过程界面。
图1-4 准备安装界面
5、稍等片刻安装完毕,进入安装结束界面,单击Finish按钮(图1-5),完成安装。
图1-5 安装结束
(三)检查是否安装成功
怎么知道是否成功安装Go语言了呢?很简单,在cmd窗口运行go命令,如果显示运行类似以下的信息证明已经成功安装Go语言了。你可以正式开始Go语言学习之旅了。
图1-6 Go安装成功后的Usage信息
二、Go基础
fallthrough
continue for
(一)定义变量
用关键字var加变量名以及变量类型 var name type
定义类型为int的a变量
同时定义多个变量
var a,b int定义定义a,b两个变量,类型都为int
当定义多个变量时也可以使用分组的方式定义多个变量
var b float32
var c string
_(下划线)是一个特殊的变量名,任何赋予它的值都会被丢弃。所以当你想输出变量_的值时会产生cannot use _ as value的错误。
变量声明但未被使用也会报错,如声明变量sum但未使用:sum declared and not used
(二)变量赋值
定义变量后给变量赋值
同时给多个变量赋值
var a,b int
a为1,b为2
(三)定义变量并初始化
var a int = 3
可以省略类型,go会根据值推导出变量的类型
在函数内部还可以使用:=来定义并赋值
必需是在函数内部定义
func main{
(四)常量
常量的类型
内置常量如true、false、iota。true和false可以直接用来初始化布尔型常量;iota初值为0,作为常量组中常量个数的计数器。
自定义常量
用户自定义的常量必须是编译器能够确定的基本数值类型,比如int型、float型、byte型、complex型、bool型或string型。
常量定义方法
与定义变量类似,定义变量时是使用var关键字,定义常量使用const关键字。常量的类型可以是数值、布尔值或字符串等类型。
1)定义与赋值分开
const Pi float32
2)定义并同时赋值
const Pi float32 = 3.14
也可以省略类型,go会根据值进行推导
const Pi = 3.14
定义常量不能省略关键字const
:=只能用在变量定义并初始化的情况,而且必需要在函数内使用。
除非被显式设置为其它值或iota,每个const分组的第一个常量被默认设置为它的0值(这个不太理解,第一个常量不赋值会报错的,怎么默认?),第二及后续的常量被默认设置为它前面那个常量的值,如果前面那个常量的值是iota,则它也被设置为iota。
(五)内置基础类型
布尔类型用bool表示,值只能是true或false,默认为false。
整数类型有无符号和有符号两种
rune int8 int16 int32 int64
byte uint8 uint16 unit32 uint64
其中rune是int32的别称,byte是uint8的别称
复数complex128 complex64
var c complex64 = 2 + 2i
浮点型float32
float64由于小数不能精确的表示,这在一些情况下会带来一些问题。比如0.1+0.2就不能精确表示结果为0.3,所以在一些重要的金融计算中,一般使用大整数进行计算,比如用整数的“分”而不是用小数的“元”来计算。0.1元+0.2元就可以这样计算:10+20
也可以用整数计算后的结果除以100转换float64(10+20)/100
(有待验证,不知道这样是否可以精确转换)
字符串类型
用string表示字符串类型,用””或“括起来表示
s := “hi!”
`括起来的字符串会原样输出,可以用于多行
s := `hello
修改字符串
字符串是不可变的,可以通过以下两种方法进行修改
先转变为[]byte类型,修改后再转回string类型
s := “hello”
c := []byte(s)
c[0] = ‘c’
s2 := string(c)
切片操作直接修改
s := “hello”
s = “c” + s[1:]
字符串遍历
字符串遍历即一个一个地访问字符串中的每一个字符,Go语言支持两种字符串遍历方式:字节数组方式遍历和Unicode字符方式遍历。
字节数组方式遍历,是按照下标顺序读取字符串中的每一个字符,类型为byte。Unicode字符方式遍历,也是按照下标顺序读取字符串中的每一个字符,但类型为rune。
package main
“fmt”
func main() {
var str string = “Hello 世界!”
n := len(str)
fmt.Println(“字节数组方式遍历:”)
for i := 0; i & i++ {
ch := str[i]
fmt.Printf(“str[%d]=%v\n”, i, ch)
fmt.Println(“Unicode字符方式遍历:”)
for i, ch := range str {
fmt.Printf(“str[%d]=%v\n”, i, ch)
字节数组方式遍历:
str[1]=101
str[2]=108
str[3]=108
str[4]=111
str[6]=228
str[7]=184
str[8]=150
str[9]=231
str[10]=149
str[11]=140
str[12]=239
str[13]=188
str[14]=129
Unicode字符方式遍历:
str[1]=101
str[2]=108
str[3]=108
str[4]=111
str[6]=19990
str[9]=30028
str[12]=65281
注:string(ch)可以把ch转换成字符。
iota默认从0开始,每调用一次加1
//z == 2 常量声明省略值时默认和之前一个值的字面相同。这里z = iota
const v = iota //每遇到一个const关键字,iota就会重置,此时v == 0
e,f,g = iota,iota,iota
//e = 0,f = 0,g = 0
这是因为iota在同一行值相同
//h = 1,I = 1,j = 1
省略值时这里必需有且只有三个常量
(六)array数组
数组的定义
var arr [n]type
var为定义变量的关键字,arr为数组名,[n]type是数组类型,其中n表示数组的长度,type表示存储元素的类型,这里应该把[n]type看成一个整体作为数组的整体类型,这是因为数组是有长度的[3]int与[5]int是两个不同的数组类型。
定义数组的一些例子:
var arr [10]int
var phone [50]string
如果没有给数组赋值默认元素为存储类型的0值,在上例中arr的10个元素全为0(int的0值为数值0),phone的50个元素全为字符串0值,即””(string的0值为空值,即””)
数组也可以使用:=来声明
a := [3]int{1,2,3}
//声明了一个长度为3的int数组
b := [5]int{1,2,3}
//声明了一个长度为5的int数组,其中前三个元素初始化为1、2、3,其它默认为0值,int的0值为数值0。
c := […]int{1,2,3}
//数组的长度也可以使用…三点表示,go会根据赋值的元素个数进行计算数组的长度,这里数组长度为3。
2)数组的访问
数组通过下标使用[]访问
var arr [10]int
arr[0] = 12
//对arr数组的第一个元素赋值,下标从0开始
arr[1] = 34
//对arr数组的第二个元素赋值
二维数组,可以理解为行与列。
doubleArray := [2][4]int{[4]int{1,2,3,4},[4]int{4,5,6,7,8}}
可以简化声明
doubleArray := [2][4]int{{1,2,3,4},{5,6,7,8}}
三维数组,可以理解为空间中X,Y,Z代表的一个点
四维数组,可以理解为空间加时间
更多维数组怎么理解有待理解,参见C语言中二维数组和指针,说到底是计算机内部存储数据的方式。
var a [2][4]int 2行4列共2*4=8个元素
var b [2][3][4]int 可以这么理解:数组b有2个元素,每个元素的类型为二维数组[3][4]int
var c [2][3][4][5]int 可以这么理解:数组c有两个元素,每个元素的类型为三维数组[3][4][5]int
多维数组类推,最左边为大的数组元素个数,后面逐级分解。
(七)slice切片
slice的声明
slice的声明与array声明类似,只是少了长度
数组切片可以基于一个已存在的数组创建。 数组切片可以只使用数组的一部分元素或者整个数组来创建,甚至可以创建一个比所基于的数组还要大的数组切片。
slice可以指向一个数组或一个已经存在的slice
// 声明一个含有11个元素元素类型为byte的数组
var Array_ori = [11]byte{‘a’, ‘b’, ‘c’, ‘d’, ‘e’, ‘f’, ‘g’, ‘h’, ‘i’, ‘j’, ‘k’}
// Slice_a指向数组的第3个元素开始,并到第五个元素结束,
Slice_a := Array_ori[2:5] //cde
//现在Slice_a含有的元素: Array_ori[2]、Array_ori[3]和Array_ori[4]
// Slice_b是数组Array_ori的另一个slice
Slice_b := Array_ori[3:5] //de
// Slice_b的元素是:Array_ori[3]和Array_ori[4]
//Slice_c指向Slice_b
Slice_c := Slice_b[1:4] //efg
//由于slice类型底层指向的是array,所以Slice_c其实指向的是Array_ori。
//Slice_c指向Slice_b,而Slice_b指向Array_ori,Array_ori[3]为Slice_b的第一个元素
//所以Slice_c实际上指向的是Array_ori[4]和Array_ori[5]、Array_ori[6]
var fslice []int
//fslice len = 0, cap = 0 不能使用的,赋值fslice[0] = 1无效,必需make初始化,//否则出现panic: runtime error: index out of range
slice := []byte{‘a’, ‘b’, ‘c’}
//slice len = cap = 3
Go语言提供的内置函数 make() 可以用于灵活地创建数组切片。
创建一个初始元素个数为5的数组切片,元素初始值为0:
slice1 := make([]int, 5)
创建一个初始元素个数为5的数组切片,元素初始值为0,并预留10个元素的存储空间:
slice2 := make([]int, 5, 10)
slice简便操作
slice的默认开始位置是0,ar[:n]等价于ar[0:n]
slice的第二个序列默认是数组的长度,ar[n:]等价于ar[n:len(ar)]
如果从一个数组里面直接获取slice,可以这样ar[:],因为默认第一个序列是0,第二个是数组的长度,即等价于ar[0:len(ar)]
slice内置函数
len获取slice的长度,即slice元素个数
cap 获取slice的最大容量,即所指向数组开始的位置到结束位置的元素个数,如果直接创建slice,go底层会默认创建一个与该slice等长的array,那么该slice的最大容量等于它的长度
append 向slice里面追加一个或者多个元素,然后返回一个和slice一样类型的slice。append函数会改变slice所引用的数组的内容,从而影响到引用同一数组的其它slice。 但当slice中没有剩余空间(即(cap-len) == 0)时,此时将动态分配新的数组空间。返回的slice数组指针将指向这个空间,而原数组的内容将保持不变;其它引用此数组的slice则不受影响。
append用法:
slice_a := make([]int,5,8)
//初始元素有5个,并且5个元素的初始值都为0,容量为8
slice_a = append(slice_a,1,2,3,4)
//把1、2、3、4共四个元素增加在slice_a切片后面并且赋值给slice_a,这时slice_a有9个元素,超出了原来slice_a的容量,go会自动分配一块足够大的内存,也就是把原来的内存块的内容拷贝到新分配的内存块中,这个过程有可能是一次或者多次,如果go第一次猜的容量不足够容纳数据再会再增加容量,所以容量的设置cap值会提高系统性能,从而达到空间换时间的效果。
函数 append() 的第二个参数其实是一个不定参数,我们可以按自己需求添加若干个元素,
甚至直接将一个数组切片追加到另一个数组切片的末尾,前提是两个slice元素类型相同:
slice_a = append(slice_a, slice_b…)
这里使用…把切片中所有的元素打散后传入
copy 函数copy从源slice的src中复制元素到目标dst,并且返回复制的元素的个数
(八)、map
map[keyType]valueType
keyType关键字类型可以是任何定义了==与!=操作的类型(不可以是func类型、array、slice、map类型),值类型可以是系统定义的类型也可以是用户定义的类型,map的声明与操作与slice类似,map的操作是通过key来操作的。
//声明变量numbers为一个map,它的key为string类型,值为int类型,注:这样声明不能直接使用必需要用make()函数初始化
var numbers map[string]int
//初始化key为string类型,值为int类型的map。注声明与初始化分开写map的类型必需要一致
numbers = make(map[string]int)
//在函数内使用:=声明与初始化map
Website := make(map[string]string)
//在函数内赋值初始化
rating := map[string]string{“A”: “优秀”, “B”: “良好”, “C”: “及格”, “D”: “不及格”}
//当然也可以赋空值map[string]string{}
//map的赋值,注意keyType、valueType分别对应
numbers[“one”] = 1
Website[“google”] = “”
//map有两个返回值,第一个返回值为key所对应的值,第二个返回值,如果不存在key,那么ok为false,如果存在则ok为true
googleWebsite, ok := Website[“google”]
fmt.Println(“google is in the map and its Website is “, googleWebsite)
fmt.Println(“We have no Website associated with google in the map”)
//用delete删除map的元素
delete(Website, “google”) //删除key为google的元素,如果不存在google则什么也不做,也不会有什么副作用
(九)、make、new操作
make用于内建类型(map、slice 和channel)的内存分配。new用于各种类型的内存分配。
make(T,args)返回初始值(非零)T类型,new(T)返回T类型的指针。
关于“零值”,所指并非是空值,而是一种“变量未填充前”的默认值,通常为0。 此处罗列 部分类型 的 “零值”
0 //rune的实际类型是 int32
0x0 // byte的实际类型是 uint8
float32 0 //长度为 4 byte
float64 0 //长度为 8 byte
“”
三、流程和函数
(一)、流程控制
go中流程控制有三大类:选择、循环、跳转
if是条件判断,它的作用是:如果满足条件就做某事,否则做另一件事。条件语句使用关键字if、else if、else
求绝对值例子:
if a & 0 {
也可以在if条件判断语句中声明一个变量,变量的作用域只能在该条件逻辑模块内:
if a := -1; a & 0 {
含有多个判断语句:
if a := 6; a == 1 {
fmt.Println(“今天是星期一”)
} else if a == 2 {
fmt.Println(“今天是星期二”)
} else if a == 3 {
fmt.Println(“今天是星期三”)
} else if a == 4 {
fmt.Println(“今天是星期四”)
} else if a == 5 {
fmt.Println(“今天是星期五”)
} else if a == 6 {
fmt.Println(“今天是星期六”)
} else if a == 7 {
fmt.Println(“今天是星期日”)
fmt.Println(“难道有星期八!?”)
if条件语句是从上往下执行语句,如果发现条件成立则执行该条件的语句而不再执行该条件模块下的语句。以上代码快会从上往下执行,判断条件是否成立,当执行到a==6时条件成立从而执行大排号里面的语句,输出:今天是星期六跳出该条件模块而不再判断下面的的条件即使下面的条件是成立的也不会再执行。else if可以是多个,一个条件模块只能有且仅有一个if块,else块可以有也可以没有,如果有的话也只能是一个。多于一个if或else都是属于另一个条件模块中的语句。
有两个if条件模块,if判断都会执行
if a == 1 {
fmt.Println(“今天是星期一”)
if a == 2 {
fmt.Println(“今天是星期二”)
switch(可以理解成入口,如果首先匹配则当做switch的第一个入口执行,执行完毕后跳出switch逻辑块。但是,该入口的语句加fallthrough则强制执行后面的case而不管是否匹配)
有些时候你需要写很多的if-else来实现一些逻辑处理,这个时候代码看上去就很丑很冗长,而且也不易于以后的维护,这个时候switch就能很好的解决这个问题。它的语法如下
switch sExpr {
case expr1:
some instructions
case expr2:
some other instructions
case expr3:
some other instructions
other code
sExpr和expr1、expr2、expr3的类型必须一致并且不能用相同的值。Go的switch非常灵活,表达式不必是常量或整数,执行的过程从上至下,直到找到匹配项,(可以理解为选择一条执行就不执行下边的了);而如果switch没有表达式,它会匹配true。
number := 2
switch number {
fmt.Println(“不买”)
case 1, 2, 3:
fmt.Println(“买小”)
case 4, 5, 6:
fmt.Println(“买大”)
fmt.Println(“其它”)
上面代码会输出“买小”。我们把很多值聚合在了一个case里面,同时,Go里面switch默认相当于每个case最后带有break,匹配成功后不会自动向下执行其他case,而是跳出整个switch, 但是可以使用fallthrough强制执行后面的case代码(这时应该把switch理解成为一个入口,入口下面的语句都会执行)。
number := 2
switch number {
fmt.Println(“不买”)
fallthrough
case 1, 2, 3:
fmt.Println(“买小”)
fallthrough
case 4, 5, 6:
fmt.Println(“买大”)
fallthrough
fmt.Println(“其它”)
number := 2
switch number {
fmt.Println(“不买”)
fallthrough
case 1, 2, 3:
fmt.Println(“买小”)
case 4, 5, 6:
fmt.Println(“买大”)
fallthrough
fmt.Println(“其它”)
number = 2,即2为switch入口,执行该条语句块,该语句块最后没有fallthrough则跳出switch逻辑模块。
goto跳转到某个标签那。
here: //这行的第一个词,以冒号结束作为标签
fmt.Println(a)
if a & 1000 {
end: //这行的第一个词,以冒号结束作为标签
fmt.Println(“The end!”)
for用于循环控制。
语法规则:
for expression1; expression2; expression3 {
expression1、expression2和expression3都是表达式,其中expression1和expression3是变量声明或者函数调用返回值之类的,expression2是用来条件判断,expression1在循环开始之前调用,expression3在每轮循环结束之时调用。在加分号时expression1、expression2和expression3中的任一个或任两个或者三个都可以为空。当expression1和expression3为空时还可以这么写:
for expression2 {
当三个都为空时可以这么写:
有些时候需要进行多个赋值操作,我们可以使用平行赋值:
for i, j := 1, 9; i & 10; i, j = i+1, j-1 {
在循环里面有两个关键操作break和continue ,break操作是跳出当前循环,continue是跳过本次循环。
for index := 10; index&0; index– {
if index == 5{
break // 或者continue
fmt.Println(index)
// break打印出来10、9、8、7、6
// continue打印出来10、9、8、7、6、4、3、2、1
for配合range可以用于读取slice和map的数据:
rating := map[string]string{“A”: “优秀”, “B”: “良好”, “C”: “及格”, “D”: “不及格”}
for k, v := range rating {
fmt.Println(“map’s key:”, k)
fmt.Println(“map’s val:”, v)
由于 Go 支持 “多值返回”, 而对于“声明而未被调用”的变量, 编译器会报错, 在这种情况下, 可以使用_来丢弃不需要的返回值 例如:
for _, v := range rating {
fmt.Println(“map’s val:”, v)
(二)、函数
1)函数定义
先看最主要的函数:main函数
func main() {}
func是函数声明的关键字,后面是函数名,声明了一个叫main的函数,小排号里是函数参数,这里参数为空。再后面可以用返回值类型或变量。大括号的语句做为函数体
一般格式:
func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
//这里是处理逻辑代码
return value1, value2 //返回多个值
函数有多个参数时,参数之间用逗号隔开“,”函数的参数也可以不定义名称,只声明其类型。当参数类型一致时可以使用省略写法,只保留最后一个参数的类型:如 a int,b int,c int等价于a,b,c int。函数的返回值定义与参数定义格式一致。当函数的返回值定义了名称,函数返回语句则可以使用return即可。
2)函数签名(signature)
一个函数的签名就是它的参数类型和返回值类型,作为程序入口的main函数没有参数也没有返回值——这是main函数的签名。我们可以使用godoc命令查看一个函数的签名及用法。如godoc math Max可以看到输出以下信息:
func Max(x, y float64) float64就是函数的签名。这样我们基本就了解了函数的用法。math包里面的Max函数接受两个float64类型的参数x、y并且返回一个float64类型的值。
Go函数支持变参。接受变参的函数是有着不定数量的参数的。
func myfunc(arg …int) {
for _, n := range arg {
fmt.Printf(“And the number is: %d\n”, n)
arg …int告诉Go这个函数接受不定数量的参数。注意,这些参数的类型全部是int。在函数体中,变量arg是一个int的slice:
函数调用:
myfunc(1,2,3)
myfunc(5,6,7,8,9)
形如 …type 格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数。它是一个语法糖(syntactic sugar) ,即这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说,使用语法糖能够增加程序的可读性,从而减少程序出错的机会。
从内部实现机理上来说,类型 …type 本质上是一个数组切片,也就是 []type ,这也是为什么上面的参数 args 可以用 for 循环来获得每个传入的参数。
假如没有 …type 这样的语法糖,开发者将不得不这么写:
func myfunc2(args []int) {
for _, arg := range args {
fmt.Println(arg)
从函数的实现角度来看,这没有任何影响,该怎么写就怎么写。但从调用方来说,情形则完全不同:
myfunc2([]int{1, 2, 3, 4})
你会发现,我们不得不加上 []int{} 来构造一个数组切片实例。但是有了 …type 这个语法糖,我们就不用自己来处理了。
4)传值与传指针
当我们传一个参数值到被调用函数里面时,实际上是传了这个值的一份副本,当在被调用函数中修改参数值的时候,调用函数中相应实参不会发生任何变化,因为数值变化只作用在副本上。
package main
import “fmt”
func add1(a int) int {
a = a + 1 // 我们改变了a的值
//返回一个新值
func main() {
fmt.Println(“x = “, x) // 应该输出 “x = 3”
x1 := add1(x) //调用add1(x)
fmt.Println(“x+1 = “, x1) // 应该输出”x+1 = 4”
fmt.Println(“x = “, x)
// 应该输出”x = 3”
传指针可以使用函数调用修改x变量的值:
package main
import “fmt”
//简单的一个函数,实现了参数+1的操作
func add1(a *int) int { // 请注意,
*a = *a + 1 // 修改了a的值
// 返回新值
func main() {
fmt.Println(“x = “, x) // 应该输出 “x = 3”
x1 := add1(&x) // 调用 add1(&x) 传x的地址
fmt.Println(“x+1 = “, x1) // 应该输出 “x+1 = 4”
fmt.Println(“x = “, x)
// 应该输出 “x = 4”
指针的特点:
传指针使得多个函数能操作同一个对象。
传指针比较轻量级 (8bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话, 在每次copy上面就会花费相对较多的系统开销(内存和时间)。所以当你要传递大的结构体的时候,用指针是一个明智的选择。
Go语言中string,slice,map这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变slice的长度,则仍需要取地址传递指针)
5)匿名函数与闭包
匿名函数是指不需要定义函数名的一种函数实现方式。
func(a, b int, z float64) bool {
return a*b & int(z)
和变量的声明不同,Go语言不能在函数里声明另外一个函数。所以在Go的源文件里,函数声明都是出现在最外层的。“声明”就是把一种类型的变量和一个名字联系起来。Go里有函数类型的变量。这样,我们虽然不可以在一个函数里直接声明另外一个函数,但可以在一个函数中声明一个函数类型的变量。此时的函数称为闭包(closure)。例如:
package main
import “fmt”
func main() {
add := func(base int) func(int) int {
return func(n int) int {
return base + n
add5 := add(5) //add5是一个函数
fmt.Println(add5(10))
add是一个闭包,因为它是无名的函数类型的变量。它会根据参数返回一个闭包。这样add5就是使用5作为add的参数得到的一个闭包。闭包的声明是在另一个函数的内部,形成嵌套。和块的嵌套一样,内层的变量可以遮盖同名的外层变量,而且外层的变量可以直接在内层使用。例如,add的base参数处在return返回的闭包的外层,所以它的值5在add返回并赋值给add5后依旧存在。当add5执行时,参数n可以和这个从外层得到的base相加,得到结果15。
package main
import “fmt”
func main() {
var j int = 5
a := func() func() {
var i int = 10
return func() {
fmt.Printf(“i, j: %d, %d\n”, i, j)
}() //这个()是前面的函数的,红色func()表示调用函数得到后面返回的函数,可以加参数进行验证,这里a相当于代表后面返回的函数
a()//表示执行上面返回的函数
上述例子的执行结果是:
i, j: 10, 5
i, j: 10, 10
上例子为啥要加()呢?我们知道普通函数调用类似这样子:afunc()这样就可以了,如果有传入参数那就是afunc(i)之类的,匿名函数没有名字,它调用时就是直接写一坨代码代码我们上面的函数名afunc。比如下面代码:
package main
import “fmt”
func main() {
func(a int) {
fmt.Println(a)
匿名函数作用是接收一个整数然后输出。再来个例子:
package main
import “fmt”
func main() {
func(a int) func() {
return func() {
fmt.Println(a)
第一个括号是一个函数调用,它传入一个参数,第二个括号是返回的函数调用,它输出第一个函数的参数!
回到开头的例子,修改一下然它们都带参数:
package main
import “fmt”
func main() {
a := func(i int) func(j int) {
return func(j int) {
fmt.Printf(“i, j: %d, %d\n”, i, j)
}(2) //执行函数,把2传入参数i,返回带参数j的匿名函数
a(7) //执行上面带参数j的匿名函数,把7传入参数j
注意:匿名函数与闭包他们的变量是可以互相访问的
Go语言中有种不错的设计,即延迟(defer)语句,你可以在函数中添加多个defer语句。当函数执行到最后时,这些defer语句会按照逆序执行,最后该函数返回。特别是当你在进行一些打开资源的操作时,遇到错误需要提前返回,在返回前你需要关闭相应的资源,不然很容易造成资源泄露等问题。
func CopyFile(dst, src string) (w int64, err error) {
srcFile, err := os.Open(src)
if err != nil {
defer srcFile.Close()
dstFile, err := os.Create(dst)
if err != nil {
defer dstFile.Close()
return io.Copy(dstFile, srcFile)
即使其中的 Copy() 函数抛出异常,Go仍然会保证 dstFile 和 srcFile 会被正常关闭。如果觉得一句话干不完清理的工作,也可以使用在 defer 后加一个匿名函数的做法:
defer func() {
// 做你复杂的清理工作
另外,一个函数中可以存在多个 defer 语句,因此需要注意的是, defer 语句的调用是遵照先进后出的原则,即最后一个 defer 语句将最先被执行,所以如下代码会输出4 3 2 1 0
for i := 0; i & 5; i++ {
defer fmt.Printf(“%d “, i)
注:defer的表达式必须是函数或方法调用。
7)函数作为值、类型
在Go中函数也是一种变量,我们可以通过type来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型。
type typeName func(input1 inputType1 , input2 inputType2 [, …]) (result1 resultType1 [, …])
package main
import “fmt”
type testInt func(int) bool // 声明了一个函数类型
//判断一个整数是否为奇数,是返回true,否返回false
func isOdd(integer int) bool {
if integer%2 == 0 {
return false
return true
//判断一个整数是否为偶数,是返回true,否返回false
func isEven(integer int) bool {
if integer%2 == 0 {
return true
return false
// 声明的函数类型在这个地方当做了一个参数
func filter(slice []int, f testInt) []int {
var result []int
for _, value := range slice {
if f(value) {
result = append(result, value)
return result
func main() {
slice := []int{1, 2, 3, 4, 5, 7}
fmt.Println(“slice = “, slice)
odd := filter(slice, isOdd) // 函数当做值来传递了
fmt.Println(“Odd elements of slice are: “, odd)
even := filter(slice, isEven) // 函数当做值来传递了
fmt.Println(“Even elements of slice are: “, even)
函数当做值和类型在我们写一些通用接口的时候非常有用,通过上面例子我们看到testInt这个类型是一个函数类型,然后两个filter函数的参数和返回值与testInt类型是一样的,但是我们可以实现很多种的逻辑,这样使得我们的程序变得非常的灵活。
8)Panic和Recover
函数签名:
func panic(interface{})
func recover() interface{}
当在一个函数执行过程中调用 panic() 函数时,正常的函数执行流程将立即终止,但函数中之前使用 defer 关键字延迟执行的语句将正常展开执行,之后该函数将返回到调用函数,并导致逐层向上执行 panic 流程,直至所属的goroutine中所有正在执行的函数被终止。错误信息将被报告,包括在调用 panic() 函数时传入的参数,这个过程称为错误处理流程。
从 panic() 的参数类型 interface{} 我们可以得知,该函数接收任意类型的数据,比如整型、字符串、对象等。调用方法很简单,下面为几个例子:
panic(404)
panic(“network broken”)
panic(Error(“file not exists”))
recover() 函数用于终止错误处理流程。一般情况下, recover() 应该在一个使用 defer
关键字的函数中执行以有效截取错误处理流程。如果没有在发生异常的goroutine中明确调用恢复过程(使用 recover 关键字) ,会导致该goroutine所属的进程打印异常信息后直接退出。
package main
import “log”
var HTTP_Status_Code = 
func foo() {
if HTTP_Status_Code ==  {
panic(&# Not Found”)
func main() {
defer func() {
if r := recover(); r != nil {
log.Printf(“Runtime error caught: %v”, r)
无论 foo() 中是否触发了错误处理流程,该匿名 defer 函数都将在函数退出时得到执行。假如 foo() 中触发了错误处理流程, recover() 函数执行将使得该错误处理过程终止。如果错误处理流程被触发时,程序传给 panic 函数的参数不为 nil ,则该函数还会打印详细的错误信息。
9) main函数和init函数
Go里面有两个保留的函数:init函数(能够应用于所有的package)和main函数(只能应用于package main)。这两个函数在定义时不能有任何的参数和返回值。虽然一个package里面可以写任意多个init函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个package中每个文件只写一个init函数。
Go程序会自动调用init()和main(),所以你不需要在任何地方调用这两个函数。每个package中的init函数都是可选的,但package main就必须包含一个main函数。
程序的初始化和执行都起始于main包。如果main包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到fmt包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行init函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,然后执行main包中的init函数(如果存在的话),最后执行main函数。下图详细地解释了整个执行过程:
10) import
import是用来导入包文件命令。如导入fmt包可以这样:
import “fmt”
也可以按分组方式导入一个或多个包:
“fmt”
“os”
导入包可以这样调用:
fmt.Println(“hello world”)
上面这个fmt是Go语言的标准库,其实是去GOROOT环境变量指定目录下去加载该模块,当然Go的import还支持如下两种方式来加载自己写的模块:
1、相对路径
import “./model”
//当前文件同一目录的model目录,但是不建议这种方式来import
2、绝对路径
import “shorturl/model”
//加载gopath/src/shorturl/model模块
上面展示了一些import常用的几种方式,但是还有一些特殊的import,让很多新手很费解,下面我们来一一讲解一下到底是怎么一回事
我们有时候会看到如下的方式导入包
. “fmt”
这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名,也就是前面你调用的fmt.Println(“hello world”)可以省略的写成Println(“hello world”)
4、别名操作
别名操作顾名思义我们可以把包命名成另一个我们用起来容易记忆的名字
f “fmt”
别名操作的话调用包函数时前缀变成了我们的前缀,即f.Println(“hello world”)
这个操作经常是让很多人费解的一个操作符,请看下面这个import
“database/sql”
_ &#/ziutek/mymysql/godrv”
_操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数。
(三)、struct类型
1、声明struct
可以声明新的类型,作为其它类型的属性或字段的容器。这样的类型我们称之为struct。例如定义类型person代表一个人的实体,这个实体拥有属性:姓名和年龄:
type person struct {
name string
用type关键字声明一个person类型的struct,name和age是person的字段,分别是string类型、int类型。name用来保存姓名这个属性,age用来保存年龄这个属性。
2、使用struct
package main
import “fmt”
type person struct {
name string
var P person // P现在就是person类型的变量了
func main() {
P.name = “Lili”
// 赋值”Lili”给P的name属性.
P.age = 18
// 赋值+给变量P的age属性
fmt.Printf(“The person’s name is %s”, P.name) // 访问P的name属性.
除了上面这种P的声明使用之外,还有另外几种声明使用方式:
1)按照顺序提供初始化值
P := person{“Tom”, 25}
2)通过field:value的方式初始化,这样可以任意顺序
P := person{age:24, name:”Tom”}
3)当然也可以通过new函数分配一个指针,此处P的类型为*person
P := new(person)
3、struct的匿名字段
上面介绍了如何定义一个struct,定义的时候是字段名与其类型一一对应,实际上Go支持只提供类型,而不写字段名的方式,也就是匿名字段,也称为嵌入字段。
当匿名字段是一个struct的时候,那么这个struct所拥有的全部字段都被隐式地引入了当前定义的这个struct。
package main
import “fmt”
type Human struct {
weight int
type Student struct {
// 匿名字段,那么默认Student就包含了Human的所有字段
speciality string
func main() {
mark := Student{Human{“Mark”, 25, 120}, “Computer Science”}
fmt.Println(“His name is “, mark.name)
// 访问相应的字段
fmt.Println(“His name is “, mark.Human.name) //另一种访问方式
fmt.Println(“His speciality is “, mark.speciality)
mark.speciality = “AI” // 修改对应的备注信息
fmt.Println(“Mark changed his speciality”)
fmt.Println(“His speciality is “, mark.speciality)
匿名字段能够实现字段的继承。Student访问Human的属性的时候,就像访问自己所有用的字段一样。当匿名字段与外层字段同名时会怎样呢?请看以下例子:
package main
import “fmt”
type Human struct {
phone string // Human类型拥有的phone字段
type Student struct {
// 匿名字段Human
phone string // 学生的phone字段
func main() {
Bob := Student{Human{“Bob”, 18, }, &#;}
fmt.Println(“Bob’s personal phone is:”, Bob.phone)
//最外层的优先访问
fmt.Println(“Bob’s home phone is:”, Bob.Human.phone) // 访问Human的phone字段
匿名字段与外层字段同名时,最外层的优先访问,多层匿名字段嵌套时情况下也是优先访问外层同名字段。
(四)、面向对象
带有接收者的函数,称为method。method是做什么用的呢?比如我们定义一个函数area()来求矩形的面积,当我们要求圆的面积时就不用再使用area()函数名了,可以取一个类型area_circle()的函数,同样计算三角形、梯形就又得更换函数名了。能不能有一种方法不用更换函数名来实现不同形状的面积呢?这其实就是method的概念。
method的语法如下:
func (r ReceiverType) funcName(parameters) (results)
方法的定义是在fun关键字之后函数名之前增加接收者信息。增加的接收者来区别不同的对象来调用不同的方法。如计算矩形、圆形面积的例子:
package main
“fmt”
“math”
type Rectangle struct {
width, height float64
type Circle struct {
radius float64
func (r Rectangle) area() float64 {
return r.width * r.height
func (c Circle) area() float64 {
return c.radius * c.radius * math.Pi
func main() {
r1 := Rectangle{12, 2}
c1 := Circle{10}
fmt.Println(“Area of r1 is: “, r1.area())
fmt.Println(“Area of c1 is: “, c1.area())
method的继承
struct的字段可以继承,method与字段一样,也可以继承。
package main
import “fmt”
type Human struct {
phone string
type Student struct {
//匿名字段
school string
//在human上面定义了一个method
func (h *Human) SayHi() {
fmt.Printf(“Hi, I am %s you can call me on %s\n”, h.name, h.phone)
func main() {
mark := Student{Human{“Mark”, 25, &#-YYYY”}, “MIT”}
mark.SayHi()
Hi, I am Mark you can call me on 222-222-YYYY
(五)、interface
1、什么是interface
什么是interface呢?简单的说,interface是一组method的组合,我们通过interface来定义对象的一组行为。interface定义也很简单,它使用type关键字来定义。
type Men interface {
定义了Men接口,它有两个方法:SayHi()与Sing()。
2、interface类型
interface类型定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口。详细的语法参考下面这个例子
type Human struct {
phone string
type Employee struct {
//匿名字段Human
company string
//Human对象实现Sayhi方法
func (h *Human) SayHi() {
fmt.Printf(“Hi, I am %s you can call me on %s\n”, h.name, h.phone)
// Human对象实现Sing方法
func (h *Human) Sing(lyrics string) {
fmt.Println(“La la, la la la, la la la la la…”, lyrics)
// Employee重载Human的Sayhi方法
func (e *Employee) SayHi() {
fmt.Printf(“Hi, I am %s, I work at %s. Call me on %s\n”, e.name,
<pany, e.phone) //此句可以分成多行
//Employee实现SpendSalary方法
func (e *Employee) SpendSalary(amount float32) {
e.money -= amount // More vodka please!!! Get me through the day!
// 定义interface
type Men interface {
Sing(lyrics string)
type ElderlyGent interface {
Sing(song string)
SpendSalary(amount float32)
通过上面的代码我们可以知道,interface可以被任意的对象实现。我们看到上面的Men interface被Human和Employee实现。同理,一个对象可以实现任意多个interface,例如上面的Employee实现了Men和ElderlyGent两个interface。
最后,任意的类型都实现了空interface(我们这样定义:interface{}),也就是包含0个method的interface。
3、interface值
那么interface里面到底能存什么值呢?如果我们定义了一个interface的变量,那么这个变量里面可以存实现这个interface的任意类型的对象。例如上面例子中,我们定义了一个Men interface类型的变量m,那么m里面可以存Human或者Employee值。
因为m能够持有这两种类型的对象,所以我们可以定义一个包含Men类型元素的slice,这个slice可以被赋予实现了Men接口的任意结构的对象,这个和我们传统意义上面的slice有所不同。
让我们来看一下下面这个例子:
package main
import &#8220;fmt&#8221;
type Human struct {
phone string
type Employee struct {
//匿名字段
company string
//Human实现SayHi方法
func (h Human) SayHi() {
fmt.Printf(&#8220;Hi, I am %s you can call me on %s\n&#8221;, h.name, h.phone)
//Human实现Sing方法
func (h Human) Sing(lyrics string) {
fmt.Println(&#8220;La la la la&#8230;&#8221;, lyrics)
//Employee重载Human的SayHi方法
func (e Employee) SayHi() {
fmt.Printf(&#8220;Hi, I am %s, I work at %s. Call me on %s\n&#8221;, e.name,
<pany, e.phone)
// Interface Men被Human和Employee实现
// 因为这两个类型都实现了这两个方法
type Men interface {
Sing(lyrics string)
func main() {
Jim := Human{&#8220;Jim&#8221;, 36, &#-XXX&#8221;}
Tom := Employee{Human{&#8220;Tom&#8221;, 37, &#-XXX&#8221;}, &#8220;Things Ltd.&#8221;, 5000}
//定义Men类型的变量i
//i能存储Human
fmt.Println(&#8220;This is Jim, a Human:&#8221;)
i.Sing(&#8220;November rain&#8221;)
//i也能存储Employee
fmt.Println(&#8220;This is Tom, an Employee:&#8221;)
i.Sing(&#8220;Born to be wild&#8221;)
//定义了slice Men
fmt.Println(&#8220;Let&#8217;s use a slice of Men and see what happens&#8221;)
x := make([]Men, 2)
//这两个都是不同类型的元素,但是他们实现了interface同一个接口
x[0], x[1] = Jim, Tom
for _, value := range x {
value.SayHi()
通过上面的代码,你会发现interface就是一组抽象方法的集合,它必须由其他非interface类型实现,而不能自我实现。
空interface
空interface(interface{})不包含任何的method,正因为如此,所有的类型都实现了空interface。空interface对于描述起不到任何的作用(因为它不包含任何的method),但是空interface在我们需要存储任意类型的数值的时候相当有用,因为它可以存储任意类型的数值。
package main
import &#8220;fmt&#8221;
func main() {
// 定义a为空接口
var a interface{}
var i int = 5
s := &#8220;Hello world&#8221;
// a可以存储任意类型的数值
fmt.Println(a)
fmt.Println(a)
输出信息:
Hello world
interface函数参数
interface的变量可以持有任意实现该interface类型的对象,这给我们编写函数(包括method)提供了一些额外的思考,我们是不是可以通过定义interface参数,让函数接受各种类型的参数。
举个例子:fmt.Println是我们常用的一个函数,但是你是否注意到它可以接受任意类型的数据。打开fmt的源码文件,你会看到这样一个定义:
type Stringer interface {
String() string
也就是说,任何实现了String方法的类型都能作为参数被fmt.Println调用,让我们来试一试。
package main
&#8220;fmt&#8221;
type Human struct {
name, sex, phone string
// 通过这个方法 Human 实现了 fmt.Stringer
func (h Human) String() string {
return h.name + &#8221; &#8211; &#8221; + h.sex + &#8221; &#8211;
? &#8221; + h.phone
func main() {
Bob := Human{&#8220;Bob&#8221;, &#8220;man&#8221;, &#7-XXX&#8221;}
fmt.Println(&#8220;This Human is : &#8220;, Bob) //不用写成Bob.String()也能调用,因为fmt.Println()会调用String()方法,而Bob则实现了自己的Sring()方法
输出信息:
This Human is :
Bob &#8211; man &#8211;
? 000-7777-XXX
如果需要某个类型能被fmt包以特殊的格式输出,你就必须实现Stringer这个接口。如果没有实现这个接口,fmt将以默认的方式输出。如上例把String()方法去掉则最后输出结果:
This Human is :
{Bob man 000-7777-XXX}
注:实现了error接口的对象(即实现了Error() string的对象),使用fmt输出时,会调用Error()方法,因此不必再定义String()方法了。
interface变量存储的类型
interface的变量里面可以存储任意类型的数值(该类型实现了interface)。那么我们怎么反向知道这个变量里面实际保存了的是哪个类型的对象呢?目前常用的有两种方法:Comma-ok断言与switch测试。
1)Comma-ok断言
Go语言里面有一个语法,可以直接判断是否是该类型的变量: value, ok = element.(T),这里value就是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型。
如果element里面确实存储了T类型的数值,那么ok返回true,否则返回false。
tips:什么是断言?
编写代码时,我们总是会做出一些假设,断言就是用于在代码中捕捉这些假设。断言表示为一些布尔表达式,程序员相信在程序中的某个特定点该表达式值为真,可以在任何时候启用和禁用断言验证,因此可以在测试时启用断言而在部署时禁用断言。同样,程序投入运行后,最终用户在遇到问题时可以重新启用断言。
package main
&#8220;fmt&#8221;
&#8220;strconv&#8221;
type Element interface{}
type List []Element
type Person struct {
name string
//定义了String方法,实现了fmt.Stringer
func (p Person) String() string {
return &#8220;(name: &#8221; + p.name + &#8221; &#8211; age: &#8221; + strconv.Itoa(p.age) + &#8221; years)&#8221;
func main() {
list := make(List, 3)
list[0] = 1
list[1] = &#8220;Hello&#8221;
list[2] = Person{&#8220;Dennis&#8221;, 70}
for index, element := range list {
if value, ok := element.(int); ok {
fmt.Printf(&#8220;list[%d] is an int and its value is %d\n&#8221;, index, value)
} else if value, ok := element.(string); ok {
fmt.Printf(&#8220;list[%d] is a string and its value is %s\n&#8221;, index, value)
} else if value, ok := element.(Person); ok {
fmt.Printf(&#8220;list[%d] is a Person and its value is %s\n&#8221;, index, value)
fmt.Println(&#8220;list[%d] is of a different type&#8221;, index)
上面value也可以丢弃掉,在输出时用element替换。
for index, element := range list {
if _, ok := element.(int); ok {
fmt.Printf(&#8220;list[%d] is an int and its value is %d\n&#8221;, index, element)
} else if _, ok := element.(string); ok {
fmt.Printf(&#8220;list[%d] is a string and its value is %s\n&#8221;, index, element)
} else if _, ok := element.(Person); ok {
fmt.Printf(&#8220;list[%d] is a Person and its value is %s\n&#8221;, index, element)
fmt.Println(&#8220;list[%d] is of a different type&#8221;, index)
(一般使用value为了防止读取外层数据使代码可读性更好吗?)
以上代码可知,断言的类型越多,那么if else也就越多,所以才引出了下面要介绍的switch。
2)switch测试
使用element.(type)判断类型。重写上面的这个实现:
package main
&#8220;fmt&#8221;
&#8220;strconv&#8221;
type Element interface{}
type List []Element
type Person struct {
name string
func (p Person) String() string {
return &#8220;(name: &#8221; + p.name + &#8221; &#8211; age: &#8221; + strconv.Itoa(p.age) + &#8221; years)&#8221;
func main() {
list := make(List, 3)
list[0] = 1
list[1] = &#8220;Hello&#8221;
list[2] = Person{&#8220;Dennis&#8221;, 70}
for index, element := range list {
switch value := element.(type) {
fmt.Printf(&#8220;list[%d] is an int and its value is %d\n&#8221;, index, value)
case string:
fmt.Printf(&#8220;list[%d] is a string and its value is %s\n&#8221;, index, value)
case Person:
fmt.Printf(&#8220;list[%d] is a Person and its value is %s\n&#8221;, index, value)
fmt.Println(&#8220;list[%d] is of a different type&#8221;, index)
for循环也可以不使用value:
for index, element := range list {
switch element.(type) {
fmt.Printf(&#8220;list[%d] is an int and its value is %d\n&#8221;, index, element)
case string:
fmt.Printf(&#8220;list[%d] is a string and its value is %s\n&#8221;, index, element)
case Person:
fmt.Printf(&#8220;list[%d] is a Person and its value is %s\n&#8221;, index, element)
fmt.Println(&#8220;list[%d] is of a different type&#8221;, index)
(一般使用value为了防止读取外层数据使代码可读性更好吗?)
这里有一点需要强调的是:element.(type)语法不能在switch外的任何逻辑里面使用,如果你要在switch外面判断一个类型就使用comma-ok。
嵌入interface
如果一个interface1作为interface2的一个嵌入字段,那么interface2隐式的包含了interface1里面的method。
如源码包container / heap里面有这样的一个定义:
type Interface interface {
sort.Interface
//嵌入字段sort.Interface
Push(x interface{}) //a Push method to push elements into the heap
Pop() interface{}
//a Pop elements that pops elements from the heap
我们看到sort.Interface其实就是嵌入字段,把sort.Interface的所有method给隐式的包含进来了。也就是下面三个方法:
type Interface interface {
// Len is the number of elements in the collection.
// Less returns whether the element with index i should sort
// before the element with index j.
Less(i, j int) bool
// Swap swaps the elements with indexes i and j.
Swap(i, j int)
另一个例子就是io包下面的 io.ReadWriter ,它包含了io包下面的Reader和Writer两个interface:
// io.ReadWriter
type ReadWriter interface {
反射指程序可以访问、检测和修改它本身状态或行为的一种能力。Go的反射是通过reflect包提供的。
package main
&#8220;fmt&#8221;
&#8220;reflect&#8221;
func main() {
var x float64 = 3.14
fmt.Println(&#8220;type:&#8221;, reflect.TypeOf(x))
fmt.Println(&#8220;value:&#8221;, reflect.ValueOf(x))
type: float64
value: &float64 Value&
先说并行,并行意思就是同时进行,无论从微观还是宏观上看它们都是同一时刻执行的。并发是宏观概念,在一段时间内它们都执行了,就比如两个人挖坑,只有一个铁锨。他们轮流挖,一个人挖了半小时累了换另一个人挖半小时,那么从宏观上讲,在一小时内两个人是并发的挖坑。如果是并行那就是他们都有铁锨各自挖,任何时刻或时间段他们挖坑都是并行的,也就是说挖坑是同时进行的。
计算机程序指令都是按顺序执行的,而且同一台计算机都是由同一晶振同步的(你可以理解为一个时间同步器,它非常快,每秒振荡几百万次以上,即我们平时说的时钟频率),那么说计算机应该没有并行的(就算多核也是同一个时间频率同步的,严格上来说也不是并发的,PS:我个人观点,别人一般认为多核算是并行),真正的并行是不同的计算机执行才算。我们这里说的并发其实时是一段时间执行一个任务下一段时间执行另一个任务,这样不断的在不同任务之间切换执行,由于这个时间非常快,人们感觉不到停顿就感觉像是“同时”运行了。
一段时间CPU执行什么任务就是由操作系统来调度的。操作系统运行的不同程序是一个进程,操作系统创建进程很慢,耗时间。go构建的程序运行也是一个进程,go创建的并发函数是一个很轻量级的东东,go进程创建后就能很快的创建并发的东东了。go创建的并发这个东东就是goroutine。
(一)、goroutine
go语言中并发的对象是一个函数,比如上面挖坑的例子,假如用a()来表示A挖坑,b()来表示B挖坑。那他们并行挖坑可以这样调用:
只要在函数调用时加上go关键字就可以了。go一个函数就创建了一个goroutine,我把它翻译成协程(啥是协程?可以理解在某个空间里执行,go a()、go b()相当于扔A在a空间里,B扔到b空间里),马克思告诉我们实践是检验真理的唯一标准,那我们实践一下吧:
package main
import &#8220;fmt&#8221;
func a() {
fmt.Println(&#8220;A在挖坑&#8221;)
func b() {
fmt.Println(&#8220;B在挖坑&#8221;)
func main() {
我们定义一个死循环,他们一直挖下去…按照上面对并发的说明,如果是并发的我们应该看到输出有时是A在挖坑有时是B在挖坑。
咦,怎么什么也没有输出啊?你这不是在坑我么?让我们来分析分析。如果去掉go关键字你能看到A在挖坑,永远看不到B在挖坑! go可执行程序都是从main()主函数开始执行的,主函数执行里面的语句,函数执行完毕后返回,也就是不执行了让下面的代码执行,这里我们看到如果调用两个函数之前的go关键字去掉(后果很严重哦!)那么b()函数根本就没机会执行,A挖上瘾了。。。
这到底哪里出了问题呢?要知道原因就要了解go的并发模型。go中main()函数退出是不等待其它协程返回就退出的,其实main()函数也是一个协程。每一个go关键字加函数相当于创建了一个协程,由于计算机运行得非常快,go创建协程也非常的快,它创建好后就不管了,a()、b()还没来得及运行main()函数执行完毕后就退出了,把所有资源回收,a()、b()就没了!原来是这样,既然main()函数本身就是一个协程,它在执行普通代码或函数返回后就退出,那我们是不是可以让函数不返回那就可以啦?是的,这里我们把go b()改为普通函数调用b()就可以了,运行一下盯着屏幕看你会看到一会是A在挖坑一会是B在挖坑,但是程序会一直运行下去,因为b()函数根本就停不下来,不停的与协程go a()交替执行,也就是我们说的并发。
我们还可以让main()函数等一会再退出,让main()函数在创建两个协程后等待1秒钟再退出(计算机运行很快,1秒钟已经很长了!)。我们使用time.Sleep(time.Second)它在time包中,作用就是睡上一秒钟。
package main
&#8220;fmt&#8221;
&#8220;time&#8221;
func a() {
fmt.Println(&#8220;A在挖坑&#8221;)
func b() {
fmt.Println(&#8220;B在挖坑&#8221;)
func main() {
time.Sleep(time.Second)
这样应该就能看到A、B挖坑了,可能你还有疑问:“我怎么还是只看到A在挖坑啊?”。好吧,还是因为计算机运行速度很快(冷汗!!),一下输出很多信息,由于一屏不能显示完这么多信息之前的信息被覆盖掉了,所以会产生只看a()或b()函数执行的结果。我们让A、B每次挖坑后睡上1毫秒再挖(如果A、B不睡上1毫秒,以现在的计算机运算速度,则会一下子就把A执行完毕了,就会看到先执行完A再去执行B了,你又会有疑问啦:看起来就不像是并发啊!还有疑问可以自己实践一下,让一个睡1毫秒另一个不睡1毫秒),并且让他们挖5次就停下来,那么一屏输出10条信息,应该就可以看到效果了吧,那就试试吧:
package main
&#8220;fmt&#8221;
&#8220;time&#8221;
func a() {
for i := 0; i & 5; i++ {
fmt.Println(&#8220;A挖坑&#8221;, i+1, &#8220;次&#8221;)
time.Sleep(time.Millisecond)
func b() {
for i := 0; i & 5; i++ {
fmt.Println(&#8220;B挖坑&#8221;, i+1, &#8220;次&#8221;)
time.Sleep(time.Millisecond)
func main() {
time.Sleep(time.Second)
这样你大概就会看到A、B交替着挖坑了吧。至于怎么交替,是A先挖还是B先挖,这就不好说了,不能保证哪个先挖,因为每次运行结果会不一样。(理论上是A先挖,因为A比B先被创建,但要注意的是计算机程序是按顺序执行代码的,我们可以肯定是先创建A协程然后再创建B协程,执行里面的内容就不能肯定了。因为创建协程非常的快,比如我们可能要创建几百万个协程A与几百万个协程B才会看到效果。比如切换协程的一个周期为创建500万个协程的时间,那么我们就创建500万个A协程,然后再创建一个B协程,B协程能在它创建的周期时间内执行完毕,那么我们是不是就看到B先输出信息啦?如果还是先执行先创建的协程那就是说永远是A协程先执行再到B再到C、、、)。如果你计算机非常慢,等待1秒钟A、B都不能执行完毕,我在想会有这么慢的电脑来做go编程么?有这么慢的电脑生产么?呵呵,开个玩笑!
虽然上面让主协程睡上一会能达到并行,但这也太不科学了吧,能不能让两个协程执行完毕后程序马上退出而不是人为的让它睡上一秒再退出,要知道运行速度是越快越好的嘛。下一节就来解决这个问题。
注意:goroutine函数并发执行,当调用函数返回时这个goroutine就结束了,如果这个调用函数有返回值,返回值会一同被丢弃掉。
(二)、channels
1、什么是channels
channels也是一种类型,channels类型变量的声明、创建与map、slice等内置类型类似,在普通的类型前面加chan关键字即可:
var ch chan int
ch = make(chan int)
cs := make(chan string)
channels你可以理解成程道——程序的隧道,协程(main主函数也是一个协程)之间通讯的隧道。可以使用发送和接收操作符 &- 向程道发送数据和从程道中接收数据。例如,ch &- 8把数字8发送给程道变量ch中,向程道中写入数据会导致协程阻塞(本身的协程被阻塞,直到数据被读出),v := &-ch变量v接收(读取)程道变量ch中的数据。向程道中读取数据也会导致本协程阻塞(也就是读取数据的协程阻塞,直到有数据接收),也就是说如果读不到数据本协程就暂停了,一直得到有数据后才返回,我们使用程序来验证一下:
package main
&#8220;fmt&#8221;
&#8220;time&#8221;
func a() {
for i := 0; i & 5; i++ {
fmt.Println(&#8220;挖坑&#8230;&#8221;)
fmt.Println(&#8220;等待5秒!&#8221;)
time.Sleep(time.Second * 5)
end &- &#8220;结束&#8221;
var end chan string
func main() {
end = make(chan string)
fmt.Println(&-end)
主函数并发的执行a(),我们在最后一个语句中调用了程道变量,如果接收不到数据它就会一直等待,它本身被阻塞了,不能返回!我们再分析一下a()函数,a()函数输出5次信息后然后等待了5秒钟最后再把数据写入程度变量end,这时end终于有数据了,fmt.Println(&-end)函数返回输出信息,程序结束。你可能有疑问我们能不能go fmt.Println(&-end)呢?让它也并发执行。尝试一下会发现程序输出信息后等待5秒,但是最后的“结束”没有被输出,这时怎么回事呢?回想一下前面的知识不难发现,goroutine也就是go关键字创建的协程,main()主函数是不等待其它的协程返回就退出的,不加go说明fmt.Println(&-end)语句是属于main()主函数的协程,加了go不再属于main()主函数的协程了。之所以能正常运行a()函数是因为fmt.Println(&-end)调用了程道,它没有收到数据前一直被阻塞了,告诉main()主协程,要收到接收到数据你才能退出!一收到数据马上退出,打印函数还没来得及执行呢!再一次证明计算机很快!注意如果把a()函数的程道变量end写在a()函数开始位置先执行会怎么样呢?结果就是a()函数程道变量end下面的代码有可能不会执行完毕,本例中会没有5秒等待,因为fmt.Println(&-end)已经接收到了程道变量end的数据就退出了,只要记住协程速度很快就会避免这种问题了,所以我们把程道变量end写在函数的末尾。需要注意的是,并发并不能改变CPU的运算速度,它只是改变了我们程序中不同函数执行的顺序而已,这样就可以让比较慢的函数让出CPU执行时间去执行其它的代码,而不用一直去等待较慢的函数执行完毕后再往下执行,对于用户来说就感觉快了。比如我们做一个网站,为了尽可能快的把一个页面内容显示给用户,假设这个页面内容很多,比如有图片、有文字、有声音、有视频等内容,而且这个页面一屏不能显示完整的页面内容,必需要拖动滚动条才可以显示完整的一个页面。这里整个页面要显示的内容就相当于main()函数,我们可以让最上面一屏先显示出来,后面的再加载,我们请文字、图片、声音、视频内容都做为一个协程并发执行,而不必等待其它内容加载再加载下一个内容,这样对于用户就会先看到有内容显示出来了,用户感觉这个网站响应速度就快了。
再看一个例子:
package main
import &#8220;fmt&#8221;
func sum(a []int, c chan int) {
total := 0
for _, v := range a {
total += v
c &- total // send total to c
func main() {
a := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(a[:len(a)/2], c)
go sum(a[len(a)/2:], c)
x, y := &-c, &-c // receive from c
fmt.Println(x, y, x+y)
2、有缓存的Channels
上面我们介绍了默认的非缓存类型的channel,不过Go也允许指定channel的缓冲大小,很简单,就是channel可以存储多少元素。ch:= make(chan bool, 4),创建了可以存储4个元素的bool 型channel。在这个channel 中,前4个元素可以无阻塞的写入。当写入第5个元素时,代码将会阻塞,直到其他goroutine从channel 中读取一些元素,腾出空间。主函数也是一个goroutine,如果它阻塞了就没有其它的goroutine读取数据程序就会出错。
ch := make(chan type, value)
value == 0 ! 无缓冲(阻塞)
value & 0 ! 缓冲(非阻塞,直到value 个元素)
package main
import &#8220;fmt&#8221;
func main() {
c := make(chan int, 1) //创建有1个缓存的Channels,如果去掉缓存或把1改成0就是无缓存的Channels
//写入数据,goroutine不阻塞
fmt.Println(&-c)
创建2个缓存的Channels:
package main
import &#8220;fmt&#8221;
func main() {
c := make(chan int, 5) //创建有2个缓存的Channels,写入第3个数据阻塞
//写入数据,goroutine不阻塞
fmt.Println(&-c)
fmt.Println(&-c)
3、Range和Close
上面这个例子中,我们需要读取两次c,这样不是很方便,如果多写一个读取c也会报错。Go考虑到了这一点,所以也可以通过range,像操作slice或者map一样操作缓存类型的channel,同时也可以使用内置的close()函数关闭程道,那接收信息一方怎么知道程道已经关闭了呢?我们可以在读取的时候使用多重返回值的方式:
x, ok := &-ch
这个用法与 map 中的按键获取 value 的过程比较类似,只需要看第二个 bool 返回值即可,如果返回值是 false 则表示 ch 已经被关闭。
请看下面生产者与消费者的例子:
package main
&#8220;fmt&#8221;
&#8220;time&#8221;
func produce(all int, ch chan int) {
defer close(ch)
for i := 1; i &= i++ {
func consumer(ch chan int) {
for msg := range ch {
fmt.Printf(&#8220;顾客:老板!我要买一部手机!,卖出%d部手机!\n&#8221;, msg)
var csum, vipsum = 0, 0
func main() {
all := 100000
ch := make(chan int, all)
go produce(all, ch)
for i := 3; i & 0; i&#8211; {
fmt.Printf(&#8220;还有%d秒开始抢购!\n&#8221;, i)
time.Sleep(time.Second)
begin := time.Now()
go consumer(ch)
v, ok := &-ch
time.Sleep(time.Second) //等待1秒防止其它协程还没来得及返回就被关闭
fmt.Printf(&#8220;VIP:老板!我要买一部手机!,卖出%d部手机!\n&#8221;, v)
vipsum += 1
end := time.Now()
t := end.Sub(begin)
fmt.Printf(&#8220;Sorry! %s 售馨,请预约下一轮!\n&#8221;, t)
fmt.Printf(&#8220;共开放%d台购买,其中顾客抢到%d台,VIP抢到%d台。\n&#8221;, csum+vipsum, csum, vipsum)
生产者每轮能生产10万台手机,定义了一个程道ch,它能缓存10万个队列,生产者生产完10万台后关闭程道,顾客有足够长的时间预约,当开始抢购时记下开始时间,顾客并发执行从程道ch中得知有没有手机,同时VIP用户也从程道ch接收数据,最后打印售馨所有时长以及下轮抢购时间,有没有很熟悉啊?
五、Web基础
六、标准包
(一)、fmt包
同一个包内源文件不同,变量、常量、函数都可调用
包内变量与函数不能同名

我要回帖

更多关于 golang map slice 的文章

 

随机推荐