您的当前位置:首页正文

Go语言基础语法

2024-11-30 来源:个人技术集锦

1、注释

// 单行注释

/*
	多行注释
	多行注释
	多行注释
	多行注释
 */

2、标识符

标识符,顾名思义就是在编程过程中,标识某种东西的符号。这个东西可以是Go语言本身的关键字、保留字,也可以是程序员自己定义的变量名、常量名、函数名、包名等。标识符由字母、数字和下划线组成,而且不能以数字开头。

// 合法标识符
user,user123,_user,_123,user_123

// 非法标识符
1user,1user123,1_user

3、关键字和保留字

关键字和保留字,是程序语言里定义的具有特殊意义的标识符。程序员编程的时候可以去使用,但是不能够去重新定义。比如去打车说去天安门,司机立马带你去了目的地。而不能说你家也叫天安门。这样的话就没法玩了。25个保留字和37个保留字。

名称描述名称描述
var变量定义关键字type自定义类型关键字
const定义常量关键字func定义函数和方法的关键字
package定义包名import导入包
if选择判断语句关键字else选择判断语句关键字
switch多重选择判断结构语句块关键字case用于switch和select语句块中,后跟一个表达式,表示一个待匹配项
fallthroughswitch语句块中使一个case子块执行完毕后,继续执行下一个case子块的代码defaultswitch和switch语句块中定义的默认项
for循环体语句块关键字continue跳过本轮循环,进入下一轮循环迭代
break强制退出循环或switch语句块return结束函数指定,使函数返回
chan用于定义channel的关键字struct结构体结构关键字
defer延迟调用关键字interface接口类型
go定义goroutine的关键字goto流程控制关键字,可以讲程序执行流程跳转到一个精确的位置
map内置map类型range定义迭代范围,常用在循环语句块中
select用于定义通信控制结构的关键字
类型保留字
Constantstrue,false,iota,nil
Typesint,int8,int16,int32,int64,uint,uint8,uint16,uint32,uint64,uintptr,float32,float64,complex64,complex128,byte,string,bool,rune,error
Functionsmake,len,cap,new,append,copy,close,delete,complex,real,imag,panic,recover

4、变量

在开发中,如果不使用变量,那么一万个页面就要编写一万个页面的代码。变量,就是在编写代码的时候,挖个坑,不同的数据来,然后填进去。

比如这个商品列表,挖坑的地方就是主图、价格、标题、销量、是否自营、店铺名称等,后期不一样的数据来了,形成永远都翻不完的商品列表。

4.1 变量的命名

1、变量是标识符的一种,所以首先得满足标识符的命名规则;

2、不能用关键字和保留字;

3、见名知意;

4、行业规范小驼峰。goodsList,userList…

4.2 变量使用

变量在编程里面就三个步骤:声明、赋值、使用。为了使用偷懒,组合,衍生出不一样的花样。比如多个变量一起声明,叫批量声明;变量和赋值合在一起,叫变量的初始化;声明和赋值合在一起,然后不写变量的数据类型,叫类型推导;声明和赋值合在一起写,然后变量声明的关键字也省了,叫短变量声明;声明和赋值合在一起,然后又进行批量赋值,而有些值又不想要了,叫匿名变量。

var 变量名 数据类型
var name string
var age int
var height float32
var sex bool

变量批量声明

var (
	name string
    age int
    height float32
    sex bool
)

变量标准赋值

变量名 = 值
name = "buddha"
age = 18
height = 1.85
sex = true  // true代表男,false代表女

变量批量赋值

变量名1,变量名2,变量名3,变量名4 =1,2,3,4

变量初始化 = 变量声明 + 变量赋值

var 变量名 数据类型 =
var name string = "buddha"
var age int = 18
var height float32 = 1.85
var sex bool = true

var (
	name string = "buddha"
	age int = 18
	height float32 = 1.85
	sex bool = true
)

变量类型推导 = 变量声明 + 变量赋值,略了变量类型

var 变量名 =
var name = "buddha"
var age = 18
var height = 1.85
var sex = true

var (
	name = "buddha"
	age = 18
	height = 1.85
	sex = true
)

var name,age,height,sex = "buddha",18,1.85,true

短变量声明 = 变量声明 + 变量赋值,省略了变量类型和省略了变量定义关键字

变量名 :=
name := "buddha"
age := 18
height := 1.85
sex := true

name,age,height,sex := "buddha",18,1.85,true

匿名变量 = 短变量声明后,用_作为不接收用不到变量的占位符

var 变量名1,_,变量名3,_ =1,值2,值3,值4

变量名1,_,变量名3,_ :=1,值2,值3,值4
name,_,height,_ := "buddha",18,1.85,true

注意点:

- 函数外定义变量需要用关键字;
- 函数外不能用短变量声明

变量的使用

变量的使用就一言难尽,可以参与其它业务运算,也可以输出到控制台等。

name,_,height,_ := "buddha",18,1.85,true

fmt.Println(name,height)  // 输出在控制台

5、常量

在开发过程中,一次性赋值,后期改变的量,叫常量。所以跟变量的区别是声明和赋值要一起,而不能说先声明,再赋值,后使用。定义常量的关键字是const。为了区分是变量和常量,所以常量的定义其关键字不能省。如果能省,那么该定义到底是变量还是常量,说不清楚了。

5.1 常量的命名

1、常量是标识符的一种,所以首先得满足标识符的命名规则;

2、不能用关键字和保留字;

3、见名知意;

4、行业规范单词全部大写,多个单词组成的常量,则用下划线隔开。如:USER,USER_NAME

5.2 常量使用

定义和使用。定义说白点就是变量里面的声明+赋值弄一起。

标准定义

const 常量名 数据类型 =
const PI float32 = 3.14

批量定义

const (
	常量名1 数据类型 =1
    常量名2 数据类型 =2
)
const (
	PI float32 = 3.14	
	TOKEN string = "e10adc3949ba59abbe56e057f20f883e"
)

类型推导

const 常量名 =const (
	常量名1 =1    
	常量名2 =2
)
const 常量名1,常量名2 =1,2
const PI = 3.14
const (
	PI = 3.14	
	TOKEN = "e10adc3949ba59abbe56e057f20f883e"
)
const (
	PI = 3.14	
	TOKEN  // 如果给PI、TOKEN赋的值相同,则第二个开始可以不写
)
const PI,TOKEN = 3.14,"e10adc3949ba59abbe56e057f20f883e"

匿名常量

const 常量名1,_,常量名3,_ =1,值2,值3,值4
const PI,_ = 3.14,"e10adc3949ba59abbe56e057f20f883e"

5.3 保留字iota

编辑器点击源码发现是这个样子

// iota is a predeclared identifier representing the untyped integer ordinal
// number of the current const specification in a (usually parenthesized)
// const declaration. It is zero-indexed.
const iota = 0 // Untyped int.

所以iota是一个系统保留字常量

// 测试1
const n1,n2,n3,n4 = iota,iota,iota,iota
fmt.Println(n1,n2,n3,n4) // 0 0 0 0
// 测试2
const (
    m1 = iota
    m2 = iota
    m3 = iota
    m4 = iota
)
fmt.Println(m1,m2,m3,m4) // 0 1 2 3
// 测试3
const (
    x1 = iota
    x2 = iota
    _ = iota
    x4 = iota
)
fmt.Println(x1,x2,x4) // 0 1 3
// 测试4
y1 := iota  // 编辑器提示错误,强制性运行报错

结论:

1、iota系统保留字,是一个常量

2、只能用在常量定义里,不能赋值给变量

3、iota值是在const里增加一行增1

6. 基本数据类型

6.1 整型

数据类型数据描述最小值最大值默认值
int8有符号8位2进制整数,取值范围:-128 ~ 127math.MinInt8math.MaxInt80
int16有符号16位2进制整数,取值范围:-32768 ~ 32767math.MinInt16math.MaxInt160
int32有符号32位2进制整数,取值范围:-2147483648 ~ 2147483647math.MinInt32math.MaxInt320
int64有符号64位2进制整数,取值范围:-9223372036854775808 ~ 9223372036854775807math.MinInt64math.MaxInt640
uint8无符号8位2进制整数,取值范围:0 ~ 2550math.MaxUint80
uint16无符号8位16进制整数,取值范围:0 ~ 655350math.MaxUint160
uint32无符号8位32进制整数,取值范围:0 ~ 42949672950math.MaxUint320
uint64无符号8位64进制整数,取值范围:0 ~ 184467440737095516150math.MaxUint640

注意:

  • uint8里的第一个字母u是unsigned无符号第一个字母
  • 目前普遍存在两种操作系统,32位和64位操作系统,当使用int和uint数据类型时,默认的取值范围就是对应操作系统的取值范围,比如32位操作系统的int对应的就是int32。在开发时,应该注意这个隐藏的坑。
  • 无符号单词是unsigned,整数单词是integer,指针单词是pointer,所以uintptr说的是存放一个指针的无符号整型
  • 为了满足不同开发场景的需要,Go1.13版本后,引入了不同进制数的表达方式,二进制则在数前面加0b,八进制则在数前面加0o,十六进制则在数字前面加0x。一个比较大的数字,可以用_分割数字方便阅读,比如1_234,表达的就是1234,数字比较短看不出好处在哪,如果数字非常大,好处就明显了。可能有人会问,怎么不用万,亿这么来,更加简便呢…不好意思,这个编程语言是国外人发明的,肯定是从他们的使用角度来设计的。1000的英文是thousand,百万的英文是million,十亿的英文是billion
  • 数据类型有默认值,所以变量的声明、赋值和使用三步骤中,赋值不是必须的。

6.2 浮点数

数据类型数据描述最大值默认值
float32有符号32位2进制小数math.MaxFloat320
float64有符号64位2进制小数math.MaxFloat640

声明一个变量是浮点数时,默认声明的是float64

f := 3.14  // float64

6.3 虚数

数据类型数据描述默认值案例
complex64复数有实部和虚部,complex64的实部和虚部为32位(0+0i)var c complex64 = 1 + 2i
complex128复数有实部和虚部,complex128的实部和虚部为64位(0+0i)var c complex128 = 1 + 2i
var c1 complex64 = 1 + 2i
fmt.Println(c1)  // (1+2i)

6.4 布尔值

数据类型数据描述默认值案例
bool布尔类型只有true和false两个值falsevar b bool = true

注意:

  • 布尔类型与其它数据类型无法进行自动转换和强制转换,也无法参与运算。
  • 布尔数据类型,只能表达对与错,真与假,两种情况的状态。
  • 因为计算机最小单位是1个字节,所以布尔数据类型在内存中是占用1个字节的

6.5 字符串

数据类型数据描述默认值案例
string用双引号" "包含的内容,就是字符串定义变量可以存放的默认空var str string = “HelloWorld”

转移字符串

用双引号引起来的内容就是字符串。但是字符串里面又要用到双引号的时候怎么办?编程语言发明者就想到用转义字符串这个办法来区分是一般双引号还是有特殊用途的双引号。其它的转义字符也是用\ + 字符来区分普通字符 or 特殊字符

转义字符描述
\r回车符(返回行首)
\n换行符(直接跳到下一行的同列位置)
\t制表符
\'单引号
\"双引号
\\反斜杠

原样输出——采用反引号``

注意:

  • 反引号里面任何字符都只是普通字符

字符串常用操作

// len() 字符串长度
var str string = "hello,world!"
mt.Println(len(str))  // 12
str := "中国人"
fmt.Println(len(str))  // 9

如果字符串内容多样,比如汉字占有3个字节,所以需要算一个字符串里字符格式则用rune关键字

str := "中国人"
fmt.Println(len([]rune(str)))  // 3
str := "hello"
fmt.Println(len([]rune(str)))  // 5
// + 字符串拼接
var str = "hello" + " world"
fmt.Println(str)

strings包下面的方法,列举几个

// strings.Trim() 去除字符串两边指定的内容
str := strings.Trim("'hello", "'")
fmt.Println(str)
// strings.TrimSpace() 去除字符串两边的空白
str := strings.TrimSpace(" hello ")
fmt.Println(str)
// strings.ToUpper() 字符串全部转换成大写
str := strings.ToUpper("hello")
fmt.Println(str)

strconv包下面的方法,列举几个

// strconv.Itoa() strconv是string和convert合成的一个函数名,意思是字符串转换,这个是int转换成字符串
str := strconv.Itoa(1234)
fmt.Println(str)

6.6 字符

​ 在计算机的世界里,只有0和1。存在磁盘里,是0和1,读到内存里也是0和1,CPU能够处理的也是0和1。那么当需要表达字母a、b、c、A、B、C等字母时,怎么办?聪明的人们搞了个ASCII代码,用65、66、67分别代表a、b、c,用97、98、99分别代表A、B、C。ASCII码一共有128个字符。

​ 当电脑普及到全世界后,发现ASCII码里没有汉字的代码,没有日语…怎么办?聪明的人们搞了一个unicode,它是unified(统一)和code(代码)搞出来的,翻译成中文就是:统一码。

​ unicode是数字和字符映射编码,类似一个厚厚的本子,上面写着,20013代表的是中国的中字。但是在计算机中如何实现呢?比如输入字符中,如何让计算机转换成20013?这个时候,UTF-8、UTF-16、UTF-32以及其它的编码来实现这个功能。其中UTF-8这个编码方式因为可以兼容ASCII而被广泛使用。

​ 字符用单引号''引起来,字符本质是一个整数,在内存中字符是一个数字。所以定义存储字符的变量,其变量的数据类型是整型,比如用byte、int8、int16、int32、int64、int。

var b int8 = 'A'
fmt.Println(b)  // 65
var c int16 = '中'
fmt.Println(c)  // 20013

如果非要在控制台输出这个字符,怎么办?转换成字符串或格式化输出

var b int8 = 'A'
fmt.Println(string(b))  // A
fmt.Printf("%c", b)  // A
var c int16 = '中'
fmt.Println(string(c))  // 中
fmt.Printf("%c", c)  // 中

格式化输出

符号描述
%c该值对应的unicode码值
%d表示为十进制显示
%T值的类型
%q该值对应的双引号括起来的go语法字符串字面值
%f显示小数

注意:

  • 在Go语言中,字符不是一种数据类型,字符只是一种特殊的整型

6.7 类型转换

Go语言中,没有自动数据类型转换,只有强制数据类型转换。

数据类型()
f := 3.14i := int8(f)
fmt.Println(i)  // 3

7. 运算符

7.1 算术运算符

运算符描述
+相加
-相减
*相乘
/相除
%求余
a := 50
b := 20
fmt.Println(a + b)  // 70
fmt.Println(a - b)  // 30
fmt.Println(a * b)  // 1000
fmt.Println(a / b)  // 2
fmt.Println(a % b)  // 10

自增和自减

a := 5
b := 7
// ++a 不合法
a++  // 等价于
a = a + 1
// var i = a++ 不合法
// --b 不合法
b--
b = b -1
// var i = b-- 不合法

注释掉的,其它编程语言可能合法,但是在Go语言里是不合法的。

7.2 关系运算符

运算符描述
==判断两个值 是否 相等,是true,否false
!=判断两个值 是否 不相等,是true,否false
>判断左边值 是否 大于 右边值,是true,否false
>=判断左边值 是否 大于等于 右边值,是true,否false
<判断左边值 是否 小于 右边值,是true,否false
<=判断左边值 是否 小于等于 右边值,是true,否false

>这个念大于,<这个念小于。如果还是记不住,就是判断开口方向,开口那一边确实大,那就是true,反之就是false

7.3 逻辑运算符

运算符描述
&&与。 运算符两边都是true则为true,否则为false
||或。 运算符两边都是false则为false,否则为true
!非。 运算符右边最终结果是true或false后,取反

短路特性

学习其它编程语言,在学习逻辑运算符的时候,最头疼的就是短路特性。而Go语言,不让你这么写。

a := 20
b := 10
c := 5
// flag := (a < b) && ((a = a -c) > b)  // 不合法
flag := a > b && b >c  // 合法
fmt.Println(flag)

7.4 位运算符

运算符描述
&位与,换算成二进制数,相同位置数字都是1的为1,否则为0
|位或,换算成二进制数,相同位置数字有是1的为1,否则为0
^位异或,换算成二进制数,相同位置数字不同的为1,相同的为0
<<位左移,换算成二进制数左移,高位数丢弃,低位数补0
>>位右移,换算成二进制数右移,高位数补0,低位数丢弃

平时接触的数学是十进制,何为十进制呢,每个位,都有0,1,2,3,4,5,6,7,8,9其中的一个,组成就是逢十进一。那么何为八进制呢,就是逢八进一。以此类推,十六进制,二进制类似。

7.5 赋值运算符

假设定义了三个变量

var a int = 20
var b int = 20
var c int
运算符描述实例
=右边表达式的结果赋值给左边变量c = a + b
+=相加后再赋值c += a 等于c = c + a
-=相减后再赋值c -= a 等于c = c - a
*=相乘后再赋值c *= a 等于c = c * a
/=相除后再赋值c /= a 等于c = c / a
%=求余后再赋值c %= a 等于c = c % a
<<=左移后赋值c <<= 2 等于c = c << 2
>>=右移后赋值c >>= 2 等于c = c >> 2
&=按位与后赋值c &= a 等于c = c & a
|=按位或后赋值c |= a 等于c = c | a
^=按位异或后赋值c ^= a 等于c = c ^ a

赋值运算符其实只有一个=,其它都是结合算术运算、位运算符演变来的。

8. 流程控制

流程控制就是指令运行时的方式。流程控制主要有三种方式,也叫三种结构,分别是顺序结构分支结构循环结构

8.1 顺序结构

从上到下,按顺序逐步进行执行。在此之前接触的,都属于顺序结构的代码。

算法

简言之,算法就是解决问题的步骤。顺序结构是最简单的算法结构。经常会有人提一个词就是,我写一个算法。说的很高大上,其实搞不好写的就是几行顺序结构的代码。

8.2 分支结构

分支结构就是,根据不同的条件,执行不同的代码块,从而得到不一样的结果。模拟一个场景,开始是家,结束是公司,可以选择不同的交通工具。这个时候用的就是分支结构。

  • if语句
if 条件表达式 {
  	执行语句
}
if 4 > 3 {
     fmt.Println("4大于3是对的")
}
  • if-else语句
if 条件表达式 {
	执行语句1
} else {
   执行语句2
}
if 4 > 3 {
	fmt.Println("4大于3是对的")
} else {
	fmt.Println("4大于3是错的")
}
  • if-else if-else语句
if 条件表达式1 {
	执行语句1
} else if 条件表达式2  {
	执行语句2
} else {
	执行语句3
}
 score := 65
 if score >= 90  {	
	 fmt.Println("成绩是优秀")
 } else if score >= 80  {	
 	fmt.Println("成绩是良好")
 } else {	
	 fmt.Println("成绩马马虎虎")
}
  • if-else嵌套语句

    if 条件表达式1 {
    	if 条件表达式2 {
    		执行语句1
    	} else {
    		执行语句2
    	}
    } else {
    	执行语句3
    }
    
    score := 95
    if score >= 60 {
    	if score >= 90 {
    		fmt.Println("成绩优秀")
    	} else {
    		fmt.Println("成绩合格")
    	}
    } else {
    	fmt.Println("成绩不及格")
    }
    
    if score := 95; score >= 60 {
    	if score >= 90 {
    		fmt.Println("成绩优秀")
    	} else {
    		fmt.Println("成绩合格")
    	}
    } else {
    	fmt.Println("成绩不及格")
    }
    

    上面这两个案例中,score := 95写法位置不一样,其实涉及的是变量的作用域的问题。

  • switch语句

    floor := 2
    switch floor {
        case 2:
        	fmt.Println("2楼停")
        case 3:
       	 fmt.Println("3楼停")
        case 4:
        	fmt.Println("4楼停")
        default:
        	fmt.Println("1楼停")
    }
    
    switch floor := 2; floor {
        case 2:
        	fmt.Println("2楼停")
        case 3:
        	fmt.Println("3楼停")
        case 4:
       	 fmt.Println("4楼停")
        default:
        	fmt.Println("1楼停")
    }
    

    switch语句默认情况下,case最后自带break语句,如果需要执行后面的case,可以使用fallthrough。

上面演示的是最常见的语句,也还可以演变出更多语句来。最终如何用,根据实际业务来定。脱离实际应用,谈哪个好,都是耍流氓。

8.3 循环结构

我每天的生活都是吃饭、睡觉、打豆豆。类似这种有规律重复的行为就用循环结构来处理。

for 初始化语句;条件语句;控制语句 {
    循环体
}

代码从上往下(程序里,代码默认是从上往下执行的)走到这个for循环结构体这。第一步:执行初始化语句;第二步:执行条件语句,如果条件语句执行结果是true则执行循环体,如果条件语句执行结果是false则跳过for循环结构往下走。假如条件语句执行结果是true,执行完毕了循环体,那么会开始执行控制语句,控制语句执行完毕则由开始去执行条件语句。重复着第二步的动作了。

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

解释:当来到这个循环体(for循环体),

第一步:执行初始化语句(i := 0);

第二步:执行条件语句(i < 3),心算过程(i 是0,然后是0 < 3,emm~~,结果是true),好的,通过心算过程得知,执行条件语句的结果是true,那么执行循环体(输出0);

第三步:执行i++(就是执行循环体后执行的控制语句),心算过程(刚才i是0,那么i++后,i是1咯),通过心算过程得知执行i++的结果是1;

第二步:执行条件语句(i < 3),心算过程(i是1,然后是1 < 3,emm,结果是true),好的,通过心算过程得知,执行条件语句的结果是true,那么执行循环体(输出1);

第三步:执行i++(就是执行循环体后执行的控制语句),心算过程(刚才i是1,那么i++后,i就是2咯),通过心算过程得知执行i++的结果是2;

第二步:执行条件语句(i < 3),心算过程(i是2,然后是2 < 3,emm,结果是true),好的,通过心算过程得知,执行条件语句的结果是true,那么执行循环体(输出2);

第三步:执行i++(就是执行循环体后执行的控制语句),心算过程(刚才i是2,那么i++后,i就是3咯),通过心算过程得知执行i++的结果是3;

第二步:执行条件语句(i < 3),心算过程(i是3,然后是3 < 3,emm,结果是false),好的,通过心算过程得知,执行条件语句的结果是false,那么不执行循环体,for循环结束,代码往下执行了。

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

上面这种写法,是把变量i的作用域提升了。注意那个;不能省略,如果省略了,就区分不出是初始化语句、条件语句还是控制语句的哪一个没写了,所以分号不能省略。

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

上面这种写法,就是告诉程序,剩下这个是条件语句,初始化语句和控制语句都去掉了。

for {    
	fmt.Println("HelloWorld")
}

上面这种写法,就是告诉程序,for里面是循环体,默认是无限循环。是否无限循环还是有条件循环,循环体来定。

i := 0
for {
    fmt.Println("HelloWorld")
    i++    
    if i >= 10 {        
    	return    
    }
}
i := 0
for {
    fmt.Println("HelloWorld")
    i++    
    if i >= 10 {
        goto End    
    }    
    End:    	
    break
}
i := 0
for {
    fmt.Println("HelloWorld")    
    i++    
    if i >= 10 {        
    	break    
   	}
}
i := 0
for {
    fmt.Println("HelloWorld")    
    i++    
    if i >= 10 {        
    	panic("end")  // 这种方式会抛出错误,业务逻辑处理时,一般不用    
   	}
}

上面这几种都是循环体里结束循环的方式。

for i := 0; i < 3; i++{
    if i == 2 {        
    	continue    
   	}    
   	fmt.Println("HelloWorld")
}

continue是结束本次循环

9. 数组

9.1 一维数组

同一种数据类型数据的集合。数组的定义和变量的定义是一样的,也是由变量关键字var + 变量名 + 数据类型组成。只不过数组的数据类型和基本数据类型有点差别。比如变量定义是这个样子var age int,而数组定义是这个样子var age [5]int。数组的数据类型[5]int是一个整体。所以呢,[4]int[5]int是不同的数据类型。

数组标准使用三个步骤,定义、赋值和使用

1. 定义var age [3]int
2. 赋值age[0] = 1age[1] = 2age[2] = 3
3. 使用fmt.Println(age)  // [1 2 3]

数组在定义的时候,默认给予初始值

var age [3]int
fmt.Println(age)  // [0 0 0]

数组也可以把定义和赋值一起

var age [3]int = [3]int{1,2,3}
fmt.Println(age)

var age = [3]int{1,2,3}  // 数据类型可以省略,可以推导得出
fmt.Println(age)

var age = [3]int{1,2}  // 初始值不赋值的,默认初始值是0
fmt.Println(age)

var age = [...]int{1,2}  // 数组长度不写,根据赋值个数来定
fmt.Println(age)

var age = [...]int{1:5, 5:8}  // 根据索引下标来赋值
fmt.Println(age)  // [0 5 0 0 0 8]

数组的遍历

var age = [...]int{1:5, 5:8}
for i := 0; i < len(age); i++ {
    fmt.Println(age[i])
}
var age = [...]int{1:5, 5:8}
for index, value := range age {
    fmt.Println(index, value)
}

值传递

a := [2]int{2,3}
var b = a
fmt.Println(a)  // [2 3]
fmt.Println(b)  // [2 3]
a[1] = 10
fmt.Println(a)  // [2 10]
fmt.Println(b)  // [2 3]

数组是值传递,不是引用传递。引用传递的特点是值发生改变,关联各个地方的值也发生改变。

9.2 多维数组

// 二维数组的定义
city := [3][2]string{
    {"北京", "上海"},
    {"南京", "杭州"},
    {"广州", "深圳"},
}

// 二维数组的遍历
for index, value := range city {
    fmt.Println(index, value)
    for k, v := range value {
        fmt.Println(k, v)
    }
}

注意:多维数组只有第一层可以使用...来规定数组长度

city := [3][...]string{  // 不合法
    {"北京", "上海"},
    {"南京", "杭州"},
    {"广州", "深圳"},
}

city := [...][3]string{  // 合法
    {"北京", "上海"},
    {"南京", "杭州"},
    {"广州", "深圳"},
}

10. 切片

切片就是可变长度的数组。

切片使用三个步骤:定义、赋值、使用

var a []int
fmt.Println(a)  // []

所以,切片默认是空数组

通过定义方式得到切片

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

通过数组方式得到切片

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

下标是1的值包含了,下标是3的值没有包含。左下标最少为0,右下标最大为切片长度-1

通过make函数得到切片

make([]T, size, cap)  // 其中size指的是切片长度,cap指的是切片容量
a := make([]int, 3, 4)
fmt.Println(a)  // [0 0 0]
fmt.Println(len(a))  // 切片长度是3
fmt.Println(cap(a))  // 切片容量是4
a := make([]int, 3, 4)
a[3] = 3
a[4] = 4
fmt.Println(a)  // 运行就会报错,因为容量是4,a[4] = 4超出了定义时切片的容量,那么怎么破呢a.append(a, 1, 2, 3)方式追加,其容量就扩大了
var a []int
a = append(a, 1)
a = append(a, 2, 3, 4)
fmt.Println(a)  // [1 2 3 4]

切片和数组的区别

// 数组
a := [...]int{1,2,3,4,5}
b := [...]int{1,2,3,4,5}
fmt.Println(a == b)  // true
// 切片
a := []int{1,2,3,4,5}
b := []int{1,2,3,4,5}
fmt.Println(a == b)  // 报错

数组是值传递,切片是引用传递。引用传递不能用关系运算符来比较。

a := []int{1,2,3,4,5}
b := a
b[0] = 20
fmt.Println(a)  // [20 2 3 4 5]
fmt.Println(b)  // [20 2 3 4 5]

引用传递,是共用数组的,一处发生改变,其它地方引用也会跟着改变。

a := []int{1, 2, 3}
b := make([]int, 3, 3)
copy(b, a)
b[0] = 20
fmt.Println(a)  // [1 2 3]
fmt.Println(b)  // [20 2 3]

利用copy函数,使得两个切片成为独立的切片,互相不受影响

判断切片是否为空

a := make([]int, 0, 4)
fmt.Println(a == nil)  // false
fmt.Println(len(a))  // 0

所以要判断一个切片是否是空的,要是用len(s) == 0来判断,不应该使用s == nil来判断。

切片遍历元素

a := []int{1,2,3,4,5}
for i := 0; i < len(a); i++ {
    fmt.Println(i, a[i])
}
a := []int{1,2,3,4,5}
for index, value := range a {
    fmt.Println(index, value)
}

切片追加元素

var a []int
a = append(a, 1)
a = append(a, 2, 3, 4)
b := []int{5, 6, 7}
a = append(a, b...)
fmt.Println(a)

可以用append为切片添加一个元素,多个元素,一个切片或多个切片

切片删除元素

a := []int{1, 2, 3, 4, 5, 6, 7, 8}
a = append(a[:2], a[3:]...)
fmt.Println(a)  // [1 2 4 5 6 7 8]

目前go1.13.15版本,切片没有专门的方法,只能通过上面类似的代码实现。切片删除实际应用中,比较常用,相信后面迭代的版本中,会添加该方法。

11. map

m := make(map[string]string)
m["username"] = "buddha"
m["age"] = "18"
m["sex"] = "male"
fmt.Println(m)  // map[age:18 sex:male username:buddha]
m := map[string]string {
    "username": "buddha",
    "age": "18",
    "sex": "male",
}
fmt.Println(m)  // map[age:18 sex:male username:buddha]

map是引用数据类型,需要初始化才能够使用。

map某个键是否存在

m := map[string]string {
    "username": "buddha",
    "age": "18",
    "sex": "male",
}
value, ok := m["username"]
fmt.Println(ok)

如果键存在ok为true,value为对应的值;不存在ok为false,value为0。value,ok只是常用的变量名,value改成v或_,ok改成flag都可以的。

map遍历

m := map[string]string {
    "username": "buddha",
    "age": "18",    
    "sex": "male",
}
// 遍历获取键值对
for k,v := range m {
    fmt.Println(k,v)
}
// 遍历获取键
for k := range m {
    fmt.Println(k)
}
// 遍历获取值
for _,v := range m {
    fmt.Println(v)
}

删除map的键值对

m := map[string]string {
    "username": "buddha",    
    "age": "18",    
    "sex": "male",
}
delete(m, "username")
fmt.Println(m)

12. 函数

实现某功能的代码集合。

函数的定义

func 函数名(形参)(返回值){
    函数体
}
  • 函数名也是标识符中的一种,所以也要符合标识符的命名规则;在同一个包内,函数名不能重名;

  • 形参由形参变量名和形参变量类型组成,多个形参用,逗号分割;

  • 返回值由返回变量和变量类型组成(如果返回值由变量名+变量类型组成,则需要用括号包裹),也可以只写返回值类型,多个返回值必须用()包裹,并用,隔开;

  • 函数体是实现指定功能的代码块。

func intSum(x int, y int) (result int) {
	result = x + y
	return result
}

func intSum(x int, y int) (result int) {
	result = x + y
	return  // 上面有写返回值变量名的,这里用return可以省略
}

func intSum(x int, y int) int {
	return x + y
}

func intSum(x, y int) int {  // 形参的类型一样,可以简写
	return x + y
}

func changeNumber(a int, b int) (x int, y int) {
	x = a
	y = b
	return y, x
}

func changeNumber(a int, b int) (int, int) {
	x := a
	y := b
	return y, x
}

func intSum(x ...int) int {  // 不定参数
	var sum int
	for _,v := range x {
		sum += v
	}
	return sum
}

函数的形参和返回值都是可选,根据实际需要来定。不定参数的本质是切片。

函数的调用

func intSum(x int, y int) int {
	return x + y
}
sum := intSum(10, 20)
fmt.Println(sum)

函数类型

可以用type关键字来定义一个函数类型,格式type 类型名称 func(形参)(返回值)

type calculation func(int, int) int
func add(x, y int) int {
	return x + y
}

func sub(x, y int) int {
	return x - y
}

下面add、sub方法都符合type calculation func(int, int) int这个格式。

package main

import "fmt"

type calculation func(int, int) int

func add(x, y int) int {
	return x + y
}

func sub(x, y int) int {
	return x - y
}

func main() {
	var c calculation
	c = add
	i := c(10, 20)
	fmt.Println(i)
}

为什么要定义函数类型呢?目的是实现多态。就是在写功能的时候把函数名当成参数传进去,参数值不同就实现不同的功能。

package main

import "fmt"

type calculation func(int, int) int

func add(x, y int) int {
	return x + y
}

func sub(x, y int) int {
	return x - y
}

func do(method calculation, x int, y int) int {
	return method(x, y)
}

func main() {
	result := do(add, 20, 30)
	fmt.Println(result)  // 50
}

函数作为参数

type op func(int int)int

上面这个就是函数类型的定义格式,func(int,int)int就是函数类型的格式。函数的形参格式是这个样子形参变量名 形参变量类型,那么函数作为函数的形参如何写?

func add(x, y int) int {
	return x + y
}

func calc(x, y int) int {
	return add(x, y)
}

在calc这个方法上添加一个函数来做形参,那额外添加一个函数名+函数类型

func add(x, y int) int {
	return x + y
}

func calc(x, y int, add func(int,int)int) int {
	return add(x, y)
}

func calc(x, y int, add func(a int, b int) int) int {
	return add(x, y)
}

函数作为返回值

func add(x, y int) int {
	return x +y
}

上面定义的函数,返回值的位置放了一个int类型,如果要让函数作为返回值,那么就是把函数的类型放那个位置

func add(x, y int) int {
	return x +y
}

func calc() func(a, b int) int {
	return add
}
f := calc()
fmt.Println(f(10, 20))  // 30

匿名函数

匿名函数,顾名思义就是没有函数名的函数

add := func(x, y int) int {
    return x + y
}
sum := add(10, 20)

fmt.Println(sum)  // 30
sum := func(x, y int) int {
    return x + y
}(10, 20)
fmt.Println(sum)  // 30

上面两种匿名函数写法的区别:第一个是在调用的时候执行,第二个是立即执行

闭包函数

闭包函数可以理解成定义在函数内部的函数

package main

import "fmt"

func add() func(int) int {
	var x int
	return func(y int) int {
		x += y
		return x
	}
}

func main() {
	var f = add()
	fmt.Println(f(10))  // 10
	fmt.Println(f(20))  // 30
	fmt.Println(f(30))  // 60
}

add()赋值给变量后,add()里面的变量和函数的生命周期就变成和文件的生命周期是一样了,变量也类似变成全局变量了。

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语句,其执行顺序是从后往前执行。

13. 指针

var str string = "hello"
ptr := &str
fmt.Println(ptr)  // 0xc0000301f0
var str string = "hello"
var ptr *string = &str
fmt.Println(ptr)  // 0xc0000301f0

指针的数据类型,就是该变量存储的数据类型前面加一个*,就比如*int*string*float32

var str string = "hello"
ptr := &str
fmt.Println(*ptr)  // hello

14. 结构体

自定义类型

type op func(int, int)int

函数类型,我们是这么定义的,那么自定义类型可以这个样子,type myInt int。这样子,myInt类型就拥有int的特性。

类型别名

type byte = uint8
type rune = int32

类型别名,编译时还是用原来的,不会产生新的类型。自定义就是会创建新的类型。

标准结构体

一个事物,要描述的清楚,则需要把事物多个维度的信息描述清楚。比如一个人,有姓名,性别,身高,体重。这些信息一填,这个人的轮廓就很清晰了。Go语言里用结构体来描述这个事物。

type 结构体类型 struct {
    字段名1 字段类型1
    字段名2 字段类型2
}
type person struct {
    username string
    sex bool
    height float32
    weight int8
}
type person struct {
	username string
	sex bool
	height float32
	weight int8
}

func main() {
	var buddha person
	buddha.username = "buddha"
	buddha.sex = true
	buddha.height = 1.80
	buddha.weight = 80
	fmt.Println(buddha)  // {buddha true 1.8 80}
	fmt.Println(buddha.username)  // buddha
}

首先要定义结构体类型,就是自定义了类型,类似一个int类型。有了类型后,接下来的步骤就是跟使用变量的步骤差不多。先变量定义,后赋值和再使用。

匿名结构体

如果定义结构体类型只用到一次,那么可以直接给变量该结构体类型

func main() {
	var buddha struct {
		username string
		sex bool
		height float32
		weight int8
	}
    
	buddha.username = "buddha"
	buddha.sex = true
	buddha.height = 1.80
	buddha.weight = 80
	fmt.Println(buddha)  // {buddha true 1.8 80}
	fmt.Println(buddha.username)  // buddha
}

new函数

i := new(int)
fmt.Println(i)  // 0xc000060090

new(类型名称)得到的是一个类型的指针。那么自定义的结构体也是一种类型,应该也是可以用new函数的。

type person struct {
	username string
	sex bool
	height float32
	weight int8
}

func main() {
	p := new(person)
	fmt.Println(p)  // &{ false 0 0}
	p.username = "buddha"
	p.sex = true
	p.height = 1.80
	p.weight = 80
	fmt.Println(p)  // &{buddha true 1.8 80}
}

对结构类型的变量,如果没有赋初始值,会根据内部其基本类型赋予默认初始值,比如bool类型是false,float32和int8是0,string类型是""

type person struct {
	username string
	sex bool
	height float32
	weight int8
}

func main() {
	p := &person{}
	fmt.Println(p)  // &{ false 0 0}
	p.username = "buddha"
	p.sex = true
	p.height = 1.80
	p.weight = 80
	fmt.Println(p)  // &{buddha true 1.8 80}
}

所以用&结构类型取址和对结构类型new操作,效果是等效的。

15. 包

包,可以理解为文件夹。文件夹下所有go文件都要在代码第一行添加package 包名来声明该文件属于哪个包。为什么要用包?解决多个文件重名的问题;更好管理和组织代码。

  • 文件声明时,只能属于某个包
  • 包名可以不和文件夹名一样,但包名不能包含-符号
  • 包名为main的文件为应用程序入口
  • 包里的标识符(变量、常量、类型、函数等)可被其它包所用,该标识符必须采用大驼峰命名。

导包

要在代码中引入其它包的内容,需要使用import关键字

import "包1"
import "包2"
import (
	"包1"
    "包2"
)
  • 导包声明通常放在package 包名声明下面
  • 导入的报名要用双引号引起来

导包取别名

为了防止导入的包重名,可以给导入的包取别名。取了别名的包,原来的报名就不可用了。

import 别名 "包"

16. 接口

接口就是定义规范

接口的定义

type 接口类型名 interface {
    方法名(形参) (返回值)
    ...
}
  • 接口类型名,一般会在单词后面加er
  • 如接口名和方法名大写,则可以被接口所在包之外的代码访问
  • 形参、返回值的变量名可省略
/* 定义接口 */
type interface_name interface {
   method_name1 [return_type]
   method_name2 [return_type]
   method_name3 [return_type]
   ...
   method_namen [return_type]
}

/* 定义结构体 */
type struct_name struct {
   /* variables */
}

/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
   /* 方法实现 */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
   /* 方法实现*/
}
package main

import (
    "fmt"
)

type Phone interface {
    call()
}

type NokiaPhone struct {
}

func (nokiaPhone NokiaPhone) call() {
    fmt.Println("I am Nokia, I can call you!")
}

type IPhone struct {
}

func (iPhone IPhone) call() {
    fmt.Println("I am iPhone, I can call you!")
}

func main() {
    var phone Phone

    phone = new(NokiaPhone)
    phone.call()

    phone = new(IPhone)
    phone.call()

}

17. 错误处理

  • Go 语言追求简洁优雅,所以,Go 语言不支持传统的 try…catch…finally 这种处理
  • Go 中引入的处理方式为:defer, panic, recover
  • Go 语言使用场景可以这么简单描述:Go 中可以抛出一个 panic 的异常,然后在 defer 中通过 recover 捕获这个异常,然后正常处理
// 使用defer + recover来捕获和处理异常
defer func() {
    err := recover()  // recover()内置函数,可以捕获到异常
    if err != nil {  // 说明捕获到错误
        fmt.Println("err=", err)
    }
}()
num1 := 10
num2 := 0
res := num1 / num2
fmt.Println(res)
显示全文