这是我参与「第五届青训营 」笔记创作活动的第1天
只不过是字节给我的任务罢了
一、本堂课重点内容:
本堂课介绍的是Golang的基础语法和实战项目讲解,基础语法包括变量、常量、类型、函数、数组、切片、Slice、Map等语法。
基础语法,都很重要,越是基础,越重要。
二、详细知识点介绍:
声明变量
var varName int32 = 1 + 6 / 3
varName := 222 // 简短声明,用于初始化和声明局部变量,类型根据表达式自动推导
var
是声明变量的关键字,varName
为变量名,int32
为变量类型,可省略,省略时编译器将会自行判断变量类型
数值类型零值为0,布尔类型零值为false
,字符串类型零值为空串,接口和引用类型的零值为nil
,数组等聚合类型零值为其元素类型对应零值
声明常量
const conNmae = value
使用const
关键字声明常量,常量的值在编译期确定
声明函数
func funcName(f float32, d float64) (int32, int64) {
return 4, 8
}
Go支持返回多个返回值。其中func
为声明函数的关键字;fucName
为函数名;(f float32, d float64)
为形参列表;(int32, int64)
为返回值列表,单个返回值可以省略括号,无返回值可省略
声明数组
var arr [5]int
arr[4] = 123
fmt.Println(arr[len(arr)-1])
第一行声明了一个int类型,元素数量为5的数组arr
第二行给数组的最后一个元素赋值100(数组下标以0起始)
第三行输出数组arr的最后一个元素,其中内置函数len()
可以用于获取数组长度(元素数量)
默认情况下,未被初始化的元素将会被默认初始化为元素类型对应的零值
声明的同时初始化
可以使用数组字面值语法对声明数组的同时初始化
var array [5]int = [5]int{1: 1, 2, 3} // 1: 1代表将下标为1的元素初始化为1,因此下标为0的元素默认为0
fmt.Println(array[0], array[1], array[2], array[3], array[4]) // 0 1 2 3 0
数组同样可以使用简短声明
shortArr := [5]int{1, 2}
fmt.Println(shortArr[0], shortArr[1], shortArr[2]) // 1 2 0
使用简短声明的同时也可以使用...
省略数组元素数量,根据字面值数量定义元素数量
shortArray := [...]int{110}// ...可省略:shortArray := []int{110}
fmt.Println(len(shortArray)) // 1
fmt.Println(shortArray[0]) // 110
fmt.Printf("%T\n", shortArray) // [1]int,使用%T可以输出对象的类型
指针
可以使用指针对函数外的变量进行修改,类似C语言的指针
func add(num *int) {
*num = 2
}
func main() {
num := 5
fmt.Println(num) // 5
replace(&num)
fmt.Println(num) // 2
}
在实际的使用过程中,会发现有的时候明明是一个指针,但是用的时候没有添加解引用符号*
,运行却还是正常的;明明是一个变量,但是将其当做指针用了,运行还是正常。其实这是编译器自动给你添加了解引用符号*
和引用符号&
,这一点在声明基于指针对象的方法处有体现
结构体
结构体是一种聚合的数据类型,可以包含多个任意类型的成员变量
type structName struct {
memberName memType
MemberName2 memType2
}
其中structName
是结构体类型的名字,memberName
和MemberName2
是成员变量的名字,memType
和memType2
分别是他们对应的类型
成员变量的名字如果是以大写字母开头的,那么其就是导出的,反之则是未导出的,导出的变量在其他包内可以进行读写
声明结构体类型的变量和声明普通变量无异,如果声明之后不对成员变量进行初始化,那么结构体变量内部的成员变量将会被默认初始化为其类型对应的零值
声明一个结构体
type Employee struct {
ID int
Name, Address string
DoB time.Time
Position string
Salary int
ManagerID int
}
声明结构体变量的同时可以使用结构体字面值对其进行初始化
employee := Employee{20230115, "HelliWrold", "CHINA", time.Date(2000, 1, 1,
0, 0, 0, 0, time.UTC).Add(8 * time.Hour), "China", 0, 1001}
fmt.Println(employee) // {20230115 HelliWrold CHINA 2000-01-01 08:00:00 +0000 UTC China 0 1001}
声明方法
可以为任意类型(包括内置类型)声明一个方法:在声明函数时,在函数名之前添加一个变量,则是一个方法,这个函数会附加到这个变量的类型上。
func (e Employee) querySalary(addNum int) {
fmt.Println(e.Salary)
}
添加的这个(e Empolyee)
是方法的接收器,使用时位于调用方法之前,语法是接收器.方法()
调用方法
employee.querySalary() // 这是调用方法
querySalary() // 这是调用包级函数
employee.addSalary(10000)
这种表达式叫做选择器,它会选择employee
这个对象合适的addSalary
方法去执行
声明基于指针对象的方法
如果想要通过一个类型的方法改变传入的实参的值,只传入实参的副本是无法改变的
那么这时候就需要使用指针了
继续根据上文的结构体Employee
进行举例,为其定义一个基于指针对象的方法
func (e *Employee) addSalary(addNum int) { // 方法的名字为(*Employee).addSalary
e.Salary += addNum
}
调用这个方法时,应该提供一个Employee
类型的指针,再通过指针调用这个方法
e := Employee{20230116, "", "", time.Time{}, "", 0, 0}
pe := &e
pe.addSalary(20)
fmt.Println(pe) // &{20230116 0001-01-01 00:00:00 +0000 UTC 20 0}
fmt.Println(*pe) // {20230116 0001-01-01 00:00:00 +0000 UTC 20 0}
fmt.Printf("%T\n", pe) // *main.Employee
但是这样使用有些繁琐,Go也有简单的用法
pe.querySalary() // 20
e.addSalary(10)
fmt.Printf("%T\n", e) // main.Employee
pe.querySalary() // 30
第2、3行代码,我们会发现这里的e
并非一个指针,但是仍然可以通过它调用addSalary
方法;第1、4行代码,pe
是一个*Employee
类型的指针,但是可以通过pe
调用一个接收器类型为Empolyee
类型的方法。
出现上文情况的原因,是因为Go的编译器根据调用的方法隐式使用&e
或*pe
,上文是隐式使用&e
调用addSalary
方法,隐式使用*pe
调用querySalary
方法
一般约定如果某个类里有一个指针作为接收器的方法,那么所有这个类的方法都必须有一个指针接收器,即使是不需要这个指针接收器的函数
声明方法时,如果一个类型名本身是一个指针的话,是不允许其出现在接收器中的 ,如:
type P *int
func (P) f() { /* ... */ } // compile error: invalid receiver type
分支语句
if-else
语法
if expression {
// TODO
} else if expression2{
// TODO
} else {
// TODO
}
这里的else if
其实是嵌套了一个if
语句
expression
的值可以是简短变量声明、赋值语句、自增表达式、函数调用
注: if语句体内部定义的变量作用域仅仅是if语句体内(包含嵌套的if语句)
switch-case
语法
switch expression {
case condition:
// TODO
case condition2:
// TODO
case conditionN:
// TODO
default:
// TODO
}
其中expression
为被判断的值,这个值可省略,如果省略则expression
默认为true
,
condition
为分支语句的条件表达式,如果expression
的值满足第一个condition
,则会执行第一个case分支的语句序列,而不会执行其他case分支,如果expression
的值不满足任何一个case分支的condition
,则执行default
子句
循环语句
for
Go语言只有for循环这一种循环语句
语法
for init expression step {
// TODO
}
for后的init expression step
可省略,如果省略则是死循环
可以使用break
中断当前循环,使用continue
跳过continue后的语句直接进入本次循环的下一次循环
如果想要跳过外层循环,可以在相应位置加上label
标签,但是这个并不常用,如同C语言的goto语句
示例
for i := 0; i < 5; i++ {
fmt.Printf("第%d次循环\n", i) // 使用Printf函数进行格式化%d和\n的用法同C语言
}
slice(切片)
slice是一个轻量级的数据结构,底层引用了一个数组对象,由指向第一个slice元素对应底层数组的指针、长度和容量三部分组成
slice的第一个元素不一定是数组的第一个元素
slice的长度不能超过容量,可以使用len()
获取slice 的长度,使用map()
获取slice的容量
可以对数组使用切片,以获取数组的部分元素,或对数组进行扩容
创建切片
slice := make([]string, 3)
slice[0] = "hello"
fmt.Println(slice) // [hello ] 一个"hello",两个空串
使用内置的make()
创建切片,在底层,make创建一个匿名数组,返回一个slice
make([]T, len)
make([]T, len, cap)
其中T
为数组类型,len
为切片长度,cap
为切片容量
第一个语句,省略了cap,这时len==cap
第二个语句,slice只引用了匿名数组的前len个元素,容量包含整个数组,用于以后的扩容
向切片追加扩容
slice = append(slice, "world!")
fmt.Println(slice) // [hello world!] 一个"hello",两个空串,一个"world"
fmt.Println(len(slice)) // 4
对数组切片
shortArray := [...]int{110, 119, 120, 114, 122}
slice := shortArray[1:5]
fmt.Println(slice) // [119 120 114 122]
slice = shortArray[:3]
fmt.Println(slice) // [110 119 120]
slice = shortArray[:]
fmt.Println(slice) // [110 119 120]
0 <= i <=j <=len(array),对数组切片,切片的内容包含原数组下标为i的元素到下标为j-1的元素
map
可以使用make()
创建map
创建map
make(map[keyT]valueT)
其中keyT是key的类型,valueT是value的类型
示例
m := make(map[string]int)
m["Devaddr"] = 0x007E1
addr, ok := m["Devaddr"]
fmt.Printf("%#x %t\n", addr, ok) // 0x7e1 true
其中ok的值代表map中是否有要访问的key
noKey, ok := m["noKey"]
fmt.Printf("%#x %t\n", noKey, ok) // 0x0 false
删除键值对
delete(m, "key")
m是要被删除键值对的map,"key"为要被删除键值对的键
delete(m, "Devaddr")
addr, ok = m["Devaddr"]
fmt.Printf("%#x %t\n", addr, ok) // 0x0 false
range
对于数组,range返回一个索引(下标)、一个value
sArray := []int{1, 2, 3, 4}
for index, value := range sArray {
fmt.Println("index:", index, "value:", value)
}
如果不想接收索引或值,也可以使用_
省略
for _, value := range sArray {
fmt.Println("value:", value)
}
类型
变量或表达式的类型定义了对应存储值的属性特征,例如数值在内存的存储大小(或者是元素的bit个数),它们在内部是如何表达的,是否支持一些操作符,以及它们自己关联的方法集等。
声明类型的语法
type typeName T
T
是底层类型
类型转换
只有当两个类型的底层基础类型相同时,才允许这种转型操作,或者是两者都是指向相同底层结构的指针类型,这些转换只改变类型而不会影响值本身
T(variable)
T是转换后的类型,variable
是被转换的变量
JSON
JSON的两个主要函数Marshal
和Unmarshal()
,分别是编码和解码操作
JSON结构体
type NodeData struct {
App string `json:"app"`
Battery int `json:"battery"`
Data string `json:"data"`
Datetime string `json:"datetime"`
Desc string `json:"desc,omitempty"`
Devaddr string `json:"devaddr"`
Fcnt int `json:"fcnt"`
Mac string `json:"mac"`
}
JSON结构体中类型后的json:"keyName,omitempty"
是成员Tag,一般用字面值的形式书写,json
后的第一部分用于指定JSON对象的名字,第二部分的omitempty
表示当结构体成员为零值时,不生成对应的对象
结构体的成员Tag可以是任意的字符串面值,但是通常是一系列用空格分隔的key:"value"键值对序列;因为值中含有双引号字符,因此成员Tag一般用原生字符串面值的形式书写。json开头键名对应的值用于控制encoding/json包的编码和解码的行为,并且encoding/...下面其它的包也遵循这个约定。成员Tag中json对应值的第一部分用于指定JSON对象的名字,比如将Go语言中的TotalCount成员对应到JSON中的total_count对象。Color成员的Tag还带了一个额外的omitempty选项,表示当Go语言结构体成员为空或零值时不生成该JSON对象(这里false为零值)。果然,Casablanca是一个黑白电影,并没有输出Color成员。
——《Go语言圣经》
使用JSON包对JSON字符串进行解析,需要将其转换为[]byte
类型再传入json.Unmarshal()
;使用json.Marshal()
将JSON结构体编码为[]byte
类型后如果需要将其输出到控制台,也需要通过类型转换转换为string
类型
示例请看json包和time包使用示例
三、实践练习例子:
四、课后个人总结:
与本人常用的C语言风格不是很像,经常突然间看不懂究竟是定义了一个数组还是别的什么东西,多了很多概念,课程讲得比较快,很多细节的东西没有明白就已经过去了,还是需要自己看Go语言圣经来补充,下面是一些课上没有讲到的补充
分号的问题
Go虽然不需要在语句末尾添加分号,实际上编译器会把特定符号后的换行符转换为分号,因此换行符的添加位置会影响Go代码的正确编译
如果行末是标识符、整数、浮点数、虚数、字符或字符串文字、关键字 break
、continue
、fallthrough
或 return
中的一个、运算符和分隔符 ++
、--
、)
、]
或 }
中的一个,如果在他们后面加换行符,则有可能会出错
Go命名风格
任何命名都要符合这个规则:以字母或下划线开头,后面可以有任意数量的字母、数字、下划线。
推荐使用驼峰式命名
Go程序的组成
Go的代码通过包(package)组织,一个包由多个源码文件组成(.go文件)
每个源代码文件,都由package package_name
声明语句起始,表示文件位于哪个包,main
包定义一个独立可执行的程序,其内部的main
函数是程序的入口
之后是导入依赖包(import package_name
)
第三部分是源代码