【Go】Go基础之语法
The Go programming language is an open source project to make programmers more productive.
Go is expressive, concise, clean, and efficient. Its concurrency mechanisms make it easy to write programs that get the most out of multicore and networked machines, while its novel type system enables flexible and modular program construction. Go compiles quickly to machine code yet has the convenience of garbage collection and the power of run-time reflection. It's a fast, statically typed, compiled language that feels like a dynamically typed, interpreted language.
变量
- 普通变量由
var
定义,常量由const
定义- 可忽略
type
指定 - 可通过
:=
简化版赋值,但变量不得再被var或const修饰,全局变量不可使用 - 可以通过(定义多个变量)
- 可忽略
- 特殊变量的变量名是
_
,代表不接收值,会被丢弃
var variableName type = value // 变量
const Pi float32 = 3.1415926 // 常量
// 简化版
vname1, vname2, vname3 := v1, v2, v3
var (
home = os.Getenv("HOME")
user = os.Getenv("USER")
gopath = os.Getenv("GOPATH")
)
类型
基础类型
- 布尔:
bool
,true或false,默认false - 数值型:
- int:
int
有符号 和uint
无符号 ,分为8,16,32,64字节类型rune
= int32,byte
= uint32
- 浮点:
float32
和float64
- 复数:
complex128
(64位实数+64位虚数)
- int:
- 字符串:
string
错误类型:error
用于处理异常信息,Go的package里面还专门有一个包errors来处理错误
err := errors.New("emit macho dwarf: elf header corrupted")
if err != nil {
fmt.Print(err)
}
数组
- [size]type []代表数组,内部定义数组大小,元素类型在外部指定
// 方式一
arr := [10]int{}
arr[0] = 1
// 方式二
arr := [10]int{5,2,3,4}
泛型Map
格式map[keyType]valueType
// 声明一个key是字符串,值为int的字典,这种方式的声明需要在使用之前使用make初始化
numbers := make(map[string]int)
// 另一种map的声明方式
var numbers map[string]int
numbers["one"] = 1 //赋值
numbers["tow"] = 2 //赋值
numbers["three"] = 3 //赋值
map
是无序的,每次打印出来的map
都会不一样,它不能通过index
获取,而必须通过key
获取map
的长度是不固定的,也就是和slice
一样,也是一种引用类型- 内置的
len
函数同样适用于map
,返回map
拥有的key
的数量 map
的值可以很方便的修改,通过numbers["one"]=11
可以很容易的把key为one
的字典值改为11
map
和其他基本型别不同,它不是thread-safe,在多个go-routine存取时,必须使用mutex lock机制- map内置有判断是否存在key的方式
- 初始化可以通过key:val的方式初始化值
- 通过delete删除map的元素
// 初始化一个字典
rating := map[string]float32{"C":5, "Go":4.5, "Python":4.5, "C++":2 }
// map有两个返回值,第二个返回值,如果不存在key,那么ok为false,如果存在ok为true
csharpRating, ok := rating["C#"]
if ok {
fmt.Println("C# is in the map and its rating is ", csharpRating)
} else {
fmt.Println("We have no rating associated with C# in the map")
}
delete(rating, "C") // 删除key为C的元素
Make、New
- make用于内建类型(map、slice 和channel)的内存分配。
- new用于各种类型的内存分配。
- new(T)分配了零值填充的T类型的内存空间,并且返回其地址:new 返回指针
- make只能创建slice、map和channel,并且返回一个有初始值(非零)的T类型:make返回初始化后的(非零)值
流程
if流程
- Go里面if条件判断语句中不需要括号
- if条件判断语句里面允许声明一个变量,这个变量的作用域只能在该条件逻辑块内,其他地方不起作用了
- 支持多条件,使用else if
if x > 10 {
fmt.Println("x is greater than 10")
} else {
fmt.Println("x is less than 10")
}
// 计算获取值x,然后根据x返回的大小,判断是否大于10。
if x := computedValue(); x > 10 {
fmt.Println("x is greater than 10")
} else {
fmt.Println("x is less than 10")
}
if integer == 3 {
fmt.Println("The integer is equal to 3")
} else if integer < 3 {
fmt.Println("The integer is less than 3")
} else {
fmt.Println("The integer is greater than 3")
}
goto流程
用goto跳转到必须在当前函数内定义的标签
func myFunc() {
i := 0
Here: //这行的第一个词,以冒号结束作为标签
println(i)
i++
goto Here //跳转到Here去
}
for流程
for expression1; expression2; expression3 {
//...
}
- expression1、expression2和expression3都是表达式
- 其中expression1和expression3是变量声明或者函数调用返回值之类的
- expression2 是用来条件判断
- expression1 在循环开始之前调用
- expression3 在每轮循环结束之时调用
- 有些时候需要进行多个赋值操作,由于Go里面没有,操作符,那么可以使用平行赋值i, j = i+1, j-1
- 有些时可以忽略expression1和expression3,
- 其中;也可以省略,类似while功能
- break和continue还可以跟着标号,用来跳到多重循环中的外层循环
- break可以配合标签使用,即跳转至标签所指定的位置
- for配合range可以用于读取slice和map的数据
sum := 1
for ; sum < 1000; {
sum += sum
}
for sum < 1000 {
sum += sum
}
for index := 10; index>0; index-- {
if index == 5{
break // 或者continue
}
fmt.Println(index)
}
// range获取
for k,v:=range map {
fmt.Println("map's key:",k)
fmt.Println("map's val:",v)
}
switch
替代多个if-else
switch sExpr {
case expr1:
some instructions
case expr2:
some other instructions
case expr3:
some other instructions
default:
other code
}
函数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kEFq1O8S-1681642454907)(./function-syntax.png)]
- 关键字
func
用来声明一个函数funcName
- 函数可以有一个或者多个参数,每个参数后面带有类型,通过
,
分隔 - 函数可以返回多个值
- 如果没有返回值,那么就直接省略最后的返回信息
- 如果有返回值, 那么必须在函数的外层添加return语句
- 同包有且只能有一个相同名称的函数,即使参数类型或个数不同都不可以同名
- 外包调用函数,需要导出函数,函数首字母大写代表导出,允许外包访问(非同包也一样)
// 多个返回值
func SumAndProduct(A, B int) (int, int) {
return A+B, A*B
}
变参
Go函数支持变参...type
func myfunc(arg ...int) {}
传值与传指针*type
- 读取数据可以使用值传参,更新数据需使用指针传参(*type指针传参,&type代表指针)
- 传指针使得多个函数能操作同一个对象。
- 传指针比较轻量级 (8bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话, 在每次copy上面就会花费相对较多的系统开销(内存和时间)。所以当你要传递大的结构体的时候,用指针是一个明智的选择。
- Go语言中
channel
,slice
,map
这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变slice
的长度,则仍需要取地址传递指针)
// 传值参数
func add1(a int) int {
a = a+1 // 我们改变了a的值
return a //返回一个新值
}
// 传指针参数
func add1(a *int) int { // 请注意,
*a = *a+1 // 修改了a的值
return *a // 返回新值
}
defer 延迟
- 延迟语句,可在函数当中增加多个defer语句,函数执行最后,会逆向执行defer语句。
- 多个defer,采用后进先出模式
- 适用于操作资源错误提前发挥,需要关闭响应资源。
func ReadWrite() bool {
file.Open("file")
defer file.Close()
if failureX {
return false
}
if failureY {
return false
}
return true
}
作为值、类型
函数也是一种变量,可以通过type定义,它的类型就是所有拥有相同的参数,相同的返回值的一种类型
type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])
// 声明了一个函数类型
type testInt func(int) bool
func isOdd(integer int) bool {
if integer%2 == 0 {
return false
}
return true
}
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
}
Main和 Init
- 都是Go的保留函数
init
能够应用于所有的package,main函数只能应用于package main- 定义时不能有任何的参数和返回值
- 虽然一个package可以些多个任意
init
,但对于可读性和可维护性而言,建议每个文件只写一个init
函数 - Go程序会自动调用
init()
和 main() ,不需要任何地方调用这两个函数 - 每个package中的
init
函数都是可选的,但package main就必须包含一个main函数 - 程序的初始化和执行都起始于main包
- 如果main包还导入了其它的包,那么就会在编译时将它们依次导入
- 有时一个包会被多个包同时导入,那么它只会被导入一次
- 当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来
- 然后再对这些包中的包级常量和变量进行初始化
- 接着执行init函数(如果有的话)
- 等所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化
- 然后执行main包中的init函数(如果存在的话)
- 最后执行main函数
面向对象
Struct
类型
声明新类型type,作为其他类型的属性或字段的容器
type person struct {
name string
age int
}
- 一个string类型的字段name,用来保存用户名称这个属性
- 一个int类型的字段age,用来保存用户年龄这个属性
// 声明方式
var P person
// 按照顺序提供初始化值
P := person{"Tom", 25}
// 通过field:value的方式初始化,这样可以任意顺序
P := person{age:24, name:"Tom"}
// 也可以通过new函数分配一个指针,此处P的类型为*person
P := new(person)
匿名字段
实际上Go支持只提供类型,而不写字段名的方式,也就是匿名字段,也称为嵌入字段
type Human struct {
name string
age int
weight int
}
type Student struct {
Human // 匿名字段,那么默认Student就包含了Human的所有字段
speciality string
}
Method
对象的函数
type Color byte
type Box struct {
width, height, depth float64
color Color
}
type BoxList []Box //a slice of boxes
// func (b Box) 代表函数属于哪个对象或struct的方法,即定义函数所属
func (b Box) Volume() float64 {
return b.width * b.height * b.depth
}
func (b *Box) SetColor(c Color) {
b.color = c
}
指针作为Receiver
- 如果一个method的receiver是*T,你可以在一个T类型的实例变量V上面调用这个method,而不需要&V去调用这个method
- 如果一个method的receiver是T,你可以在一个T类型的变量P上面调用这个method,而不需要 P去调用这个method
- 你不用担心你是调用的指针的method还是不是指针的method,Go知道你要做的一切
Method继承
匿名字段实现了一个method,则包含匿名字段的struct
也能调用该method
type Human struct {
name string
age int
phone string
}
type Employee struct {
Human //匿名字段
company string
}
//Human定义method
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
//Employee的method重写Human的method
func (e *Employee) SayHi() {
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
e.company, e.phone) //Yes you can split into 2 lines here.
}
Method重写
//Employee的method重写Human的method
func (e *Employee) SayHi() {
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
e.company, e.phone) //Yes you can split into 2 lines here.
}
Interface接口
interface类型定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口
// 定义interface
type Men interface {
SayHi()
Sing(lyrics string)
}
interface值
若一个struct
类型的方法都实现了interface的接口,则interface可存储该struct
类型的对象
//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...", lyrics)
}
mike := Human{"Mike", 25, "222-222-XXX"}
sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000}
//定义Men类型的变量i
var i Men
//i能存储Employee、Human
i = sam
i = mike
空interface
不包含任何方法,故所有类型都实现了空interface,对于描述起不到任何作用,但可以存储任意类型数值
// 定义a为空接口
var a interface{}
var i int = 5
s := "Hello world"
// a可以存储任意类型的数值
a = i
a = s
interface函数参数
interface的变量可以持有任意实现该interface类型的对象,如fmt.Stringer
type Stringer interface {
String() string
}
interface变量存储的类型
反向获取变量类型
- Comma-ok断言
- Go语言里面有一个语法,可以直接判断是否是该类型的变量: value, ok = element.(T),这里value就是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型。
- 如果element里面确实存储了T类型的数值,那么ok返回true,否则返回false。
- switch测试
switch value := element.(type) { case int: fmt.Printf("list[%d] is an int and its value is %d\n", index, value) case string: fmt.Printf("list[%d] is a string and its value is %s\n", index, value) case Person: fmt.Printf("list[%d] is a Person and its value is %s\n", index, value) default: fmt.Println("list[%d] is of a different type", index) }
嵌入interface
如果一个interface1作为interface2的一个嵌入字段,那么interface2隐式的包含了interface1里面的method
type Interface interface {
sort.Interface //嵌入字段sort.Interface,把sort.Interface的所有method给隐式的包含进来了
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
type Interface interface {
// Len is the number of elements in the collection.
Len() int
// 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)
}
反射
Go语言实现了反射,所谓反射就是能检查程序在运行时的状态,一般用到的包是reflect包
- 首先需要把它转化成reflect对象(
reflect.Type
或者reflect.Value
,根据不同的情况调用不同的函数) - 将reflect对象转化成相应的值
- 获取反射值能返回相应的类型和数值
- 反射类型Elem获取字段的标签,即字段名
- 反射数值Elem获取字段的值
- 反射的话,那么反射的字段必须是可修改的
t := reflect.TypeOf(i) //得到类型的元数据,通过t我们能获取类型定义里面的所有元素
v := reflect.ValueOf(i) //得到实际的值,通过v我们获取存储在里面的值,还可以去改变值
tag := t.Elem().Field(0).Tag //获取定义在struct里面的标签
name := v.Elem().Field(0).String() //获取存储在第一个字段里面的值
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())
// 以下修改字段值是错误的
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1)
// 以下修改字段值是正确的,反射时需要使用 & 修饰变量才能对反射后的变量进行修改
var x float64 = 3.4
p := reflect.ValueOf(&x)
v := p.Elem()
v.SetFloat(7.1)
import导入和pacage包
- import 导入时,若只有一个导入项可省略括号,当多个是,都在括号中包含
import (
"math"
"fmt"
)
测试
- 定义{file_name}_test.go代表测试文件
- 内部定义Test{Name}的函数,参数为(t *testing.T),该函数用于
go test
测试
例如kk_test.go:
package main
import (
"fmt"
"testing"
)
// 测试函数必需以Test开头,并且参数为(t *testing.T),固定模式!
func TestKK(t *testing.T) {
fmt.Println("PASS TEST")
}
D:\src>go test
PASS TEST
PASS
ok demo 0.230s
Fuz模糊测试
- 模糊目标应该是快速和确定的,这样模糊引擎才能有效地工作,并且可以很容易地重现新的故障和代码覆盖。
- 由于模糊目标是在多个工作者之间以不确定的顺序并行调用的,因此模糊目标的状态不应该持续到每次调用结束之后,并且模糊目标的行为不应该依赖于全局状态。
测试文件中定义Fuzz{Name}的函数,该函数代表模糊测试,用于go test -fuzz=Fuzz
做模糊测试
func FuzzReverse(f *testing.F) {
testcases := []string {"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc) // Use f.Add to provide a seed corpus
}
f.Fuzz(func(t *testing.T, orig string) {
// Reverse 是程序的一个函数,无需关心,下面的逻辑是模糊测试
rev, err1 := Reverse(orig)
if err1 != nil {
return
}
doubleRev, err2 := Reverse(rev)
if err2 != nil {
return
}
if orig != doubleRev {
t.Errorf("Before: %q, after: %q", orig, doubleRev)
}
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
}
})
}
D:\src>go test -fuzz=Fuzz
PASS TEST
fuzz: elapsed: 0s, gathering baseline coverage: 0/10 completed
fuzz: minimizing 38-byte failing input file
fuzz: elapsed: 0s, gathering baseline coverage: 4/10 completed
--- FAIL: FuzzReverse (0.16s)
--- FAIL: FuzzReverse (0.00s)
reverse_test.go:43: Number of runes: orig=1, rev=1, doubleRev=1
reverse_test.go:45: Before: "\xb7", after: "�"
Failing input written to testdata\fuzz\FuzzReverse\72a3fe19fabb4e967f79fa5802976a563b6502c1ee5a29dca84ad5a146de8634
To re-run:
go test -run=FuzzReverse/72a3fe19fabb4e967f79fa5802976a563b6502c1ee5a29dca84ad5a146de8634
FAIL
exit status 1
FAIL demo 0.362s
D:\src>go test -run=FuzzReverse/72a3fe19fabb4e967f79fa5802976a563b6502c1ee5a29dca84ad5a146de8634