语法
1. 注释
// 单行注释
/*
多行注释
多行注释
*/
// 函数注释
// 函数名 说明
// max 两个数字比大小
func max(num1, num2 int) int {
// function_body...
}
2. 变量和常量
2.1. 变量
在Go语言中,声明一个变量一般使用var
关键字。
变量的命名规则都遵循小驼峰命名法。
// var 变量名 变量类型
var name string = "go"
var age int = 18
fmt.Println(name, age)
在C语言中存在含糊不清的声明形式,如:int* a, b;
。其中a是指针类型,而b不是,如果需要两个都是指针,这需要分开书写。在Go中,可以使用下面的语法将它们都声明为指针类型。
var a, b *int
批定义变量
使用关键字var和括号,可以将一组变量定义放在一起。
var (
addr string
phone string
)
fmt.Println(addr, phone)
var形式的声明语句往往是用于需要显示指定变量类型的地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。
当一个变量被声明后,如果没有显示的给他赋值,系统自动赋予它该类型的零值。
- 整型和浮点型变量的默认值为0和0.0.
- 字符串变量的默认值为空字符串。
- 布尔型变量默认为false。
切片、函数、指针变量默认为nil。
变量的初始化
变量初始化的标准格式
// var 变量名 类型 = 值(表达式) var name string = "go" var age int = 18 fmt.Printf("name: %s, age: %d", name, age)
短变量声明并初始化
name := "go" age := 18 fmt.Printf("name: %s, age: %d", name, age) // 打印变量的类型 fmt.Printf("name: %T, age: %T", name, age) // 打印变量的内存地址 fmt.Printf("name: %p, age: %p", &name, &age)
这是Go语言的推导声明写法,编译器会自动根据右值类型推断出左值的对应类型。
它可以自动的推导出一些类型,但是使用也是有限制的:
- 定义变量,同时显式初始化。
- 不能提供数据类型。
- 只能用在函数内部。不能随便到处定义。
因为简洁和灵活的特点,短变量声明被广泛用于大部分的局部变量的声明和初始化。
变量交换。
var a int = 100 var b int = 200 // a, b交换值 b, a = a, b
匿名变量
匿名变量的特点是一个下划线
_
,_
本身就是一个特殊的标识符,被称为空白标识符。它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。使用匿名变量时,只需要在变量声明的地方使用下划线替换即可。package main import "fmt" func test() (int, int) { return 100, 200 } func main() { // 只需要获取第一个返回值,所以第二个返回值定义为匿名变量 a, _ := test() // 只需要获取第二个返回值,所以第一个返回值定义为匿名变量 _, b := test() fmt.Println(a, b) }
在编码的过程中,可能会遇到没有名称的变量、类型或方法。虽然不是必须的,但有时候这样做可以极大地增强代码的灵活性,这个变量被统称为匿名变量。
匿名变量不占用内存空间,不会分配内存。匿名变量与匿名变量之间也不会因为多次声明而无法使用。
变量的作用域
局部变量
在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,函数的参数和返回值变量都属于局部变量。
全局变量
在函数体外声明的变量称之为全局变量,全局变量只需要在一个源文件中定义,就可以在所有源文件中使用。当然,不包含这个全局变量的源文件需要使用
import
关键字引入全局变量所在的源文件之后才能使用这个全局变量。全局变量声明必须以
var
关键字开头,如果想要在外部包中使用的全局变量的首字母必须大写。
2.2. 常量
常量是在程序运行时,不会被过修改的量,可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。
const identifier [type] = value
可以省略类型说明符[type]
,因为编译器可以根据变量的值来推断其类型。
- 显式类型定义:
const b string = "abc"
- 隐式类型定义:
const b = "abc"
多个声明可以简写:
const c_name1, c_name2 = value1, value2
const URL string = "www.baidu.com"
const URL2 = "www.baidu.com"
const A, B, C = 3.14, "hello", false
2.3. iota
iota,特殊常量,可以认为是一个可以被编译器修改的常量。iota是Go语言的常量计数器。
iota在const关键字出现时将被重置为0(const内部的第一行之前),const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。
iota可以被用作枚举值
const (
a = iota
b = iota
c = iota
)
第一个ioa等于0,每当iota在新的一行被使用时,它的值都会自动加1;所以可以简写成:
const (
a = iota
b
c
)
在const关键字出现时就为0,每新增一个常量就会增加1,无论有无赋值。
如常量无赋值,则值为跟着上一次赋值的值。
package main
import "fmt"
func main() {
const (
j = "haha" // haha
a = iota // 1
b // 2
c // 3
d = "haha" // haha
e // haha
f = 100 // 100
g // 100
h = iota // 8
i // 9
)
fmt.Println(j, a, b, c, d, e, f, g, h, i)
}
3. 基本数据类型
Go语言是一种静态类型的编程语言,在Go语言中,数据类型用于声明函数和变量。数据类型的出现是为了把数据分成所需内存大小不同的数据,编程的时候需要用大数据的时候才申请大内存,就可以充分利用内存。编译器在编译的时候,就要知道每个值的类型,这样编译器就知道要为这个值分配多少的内存,并且知道这段分配的内存表示什么。
基本数据类型:
布尔型bool
bool默认值为false
var isFlag bool fmt.Println(isFlag) fmt.Printf("%T, %t", isFlag, isFlag) // truebool, true
数值型
Go语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码。Go也有基于架构的类型,例如uInt无符号、int有符号。
整型(int, int8, int16, int32, int64, uInt, uIntptr, uInt8, byte, uInt16, uInt32, uInt64)
无符号范围:
0 到 2^n - 1
有符号范围:
-2^(n-1) 到 2^(n-1)-1
浮点型(float32, float64, complex64, complex128)
floatN:IEEE-754 N位浮点数
complexN
N/2
位实数和虚数package main import "fmt" func main() { var age int = 18 fmt.Printf("%T, %d\n", age, age) var money float64 = 3.14 fmt.Printf("%T, %f\n", money, money) // 保留两位小数,会丢失精度四舍五入 fmt.Printf("%T, %.2f\n", money, money) // int, 18 // 默认是6位小数打印 // float64, 3.140000 // float64, 3.14 }
浮点数=符号位 + 指数位 + 尾数位
尾数位部分可能丢失,造成精度损失。尽量使用
float64
来定义浮点类型的小数。var num1 float32 = -123.0000901 var num2 float64 = -123.0000901 fmt.Println("num1 = ", num1, ", num2 = ", num2) // num1 = -123.00009 , num2 = -123.0000901
更多数字类型
| 类型 | 描述 | | ------- | ----------------------------- | | byte | 类似uInt8 | | rune | 类似Int32 | | uInt | 32或64位 | | int | 与uInt一样大小 | | uIntPtr | 无符号整型,用于存放一个指针+ |
字符串型string
Go中的字符串是一个字节的切片,可以通过将其内容封装在""中来创建字符串。Go中的字符串是Unicode兼容的,并且是UTF-8编码。字符串是一些字节的集合。
// 单双引号的不同 string char v1 := 'A' v2 := "A" fmt.Printf("%T, %d\n", v1, v1) // int32, 65 fmt.Printf("%T, %s\n", v2, v2) // string, A // 字符串拼接 var s1 string = "hello" var s2 string = "world" fmt.Println(s1 + s2) // 特殊字符转义相同 str := "hello, golang" fmt.Println(str) // 获取字符串的长度len fmt.Println(len(str)) // 获取指定下标下的字节 fmt.Println(str[0]) fmt.Printf("%c", str[0])
派生数据类型
- 指针
- 数组
- 结构体
- 通道(channel)
- 切片(slice)
- 函数
- 接口(interface)
- Map
3.1. 数据类型转换
所有数据类型的转换都必须显式声明。
// valueOfTypeB = typeB(valueOfTypeA)
a := 5.0 // float
b := int(a)
fmt.Printf("%T, %f\n", a, a)
fmt.Printf("%T, %d\n", b, b)
// float64, 5.000000
// int, 5
类型转换只能在定义正确的情况下转换成功,例如从一个取值范围较小的类型转换到一个取值范围较大的类型(将int16转换为int32)。当从一个取值范围较大的类型转换取值范围较小的类型时(将int32转换为int16或将float32转换为int),则会发生精度丢失(截断)的情况。
整型不能转换为bool类型。
4. 运算符
算术运算符
+, -, *, /, %, ++, --
关系运算符
==, !=, >, <, >=, <=
逻辑运算符
&&, ||, !
位运算符
&, ^, &^, <<, >>
&^
位清空,a &^ b
, 对于b上的每个数值,如果为0,这取a对应位上的数值,如果为1,则取0.赋值运算符
=, +=, -=, *=, /=, %=, <<=, >>=, &=, |=, ^=
其他运算符
&
取址运算符。*
取值运算符。
5. 输入与输出
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
var x int
var y float64
fmt.Println("请输入两个数: 1:整数,2:浮点数")
// 可以通过地址来修改和操作变量
// 使用地址接收
fmt.Scanln(&x, &y)
fmt.Println(x, y)
// 使用键盘录入变量
//fmt.Scanln()
//fmt.Scanf()
//fmt.Scan()
// 读取string
reader := bufio.NewReader(os.Stdin)
str, _ := reader.ReadString('\n')
fmt.Println("读取的内容为:", str)
}
6. 编码规范
6.1. 代码规范
命名规范
包名package
保持package名字和目录名一致,尽量采取有意义的包名,不要和标准库冲突。应该为小写单词,不要使用下划线或者混合大小写。
package mode1 package main
文件名
尽量采取有意义的文件名,应该为小写单词,使用下划线分隔各个单词(蛇形命名)。
注释规范
统一使用中文注释,对于中英文字符和标点之间严格使用空格分隔。
6.2. import规范
import在多行的情况下,会自动格式化。
如果引入了三种类型的包,标准库包,程序内部包,第三方包,建议采用如下方式组织包。
import(
"encoding/json"
"string"
"myproject/models"
"myproject/controller"
"myproject/utils"
"github.com/astaxie/beego"
"github.com/go-sql-driver/mysql"
)
有顺序的引入包,不同的类型采用空行分离。
不要使用相对路径引入包,如import "../net"
.
但是如果是引入本项目中的其他包,最好使用相对路径。
7. 流程控制
顺序结构、选择结构,循环结构。
顺序结构:从上到下,逐行执行。
选择结构:
if
var score = 15 if score > 20 { fmt.Println("score > 20") } else if score > 10 && score < 20 { fmt.Println("score > 10 < 20") } else { fmt.Println("score <= 10") }
switch
var score = 15 switch score { case 10, 11, 12: fmt.Println("score = 10") case 15: fmt.Println("score = 15") default: fmt.Println("others") } // switch 默认条件 bool = true switch { case true: fmt.Println(true) default: fmt.Println(false) }
switch默认情况下匹配成功后就不会执行其他case,如果需要执行后面的case,可以使用
fallthrough
穿透case。使用fallthrough
会强制执行后面的case语句,不会再判断下一条case的表达式结果是否为true。switch { case true: fmt.Println(true) fallthrough case false: fmt.Println(false) } // 会同时输出true和false
break可以终止穿透。
select
循环结构
for
for i := 1; i <= 5; i++ { fmt.Println(i) } i := 1 for i <= 5 { fmt.Println(i) i++ } // 死循环 //for { // //} for i := 0; i < len(str); i++ { fmt.Printf("%c", str[i]) } // for range循环,遍历数组,切片... for i := range str { fmt.Println(str[i]) } for index, value := range str { fmt.Print(index) fmt.Printf("%c", value) }
break和continue
break 结束当前整个循环
continue 结束当次循环
8. 函数
- Go语言最少有个
main()
函数。
函数定义格式如下:
func functiuon_name( [parameter list] ) [return_types] {
function_body
}
package main
import "fmt"
func main() {
printInfo()
myPrint("haha")
c := sub(1, 2)
fmt.Println(c)
a, b := swap(1, 2)
fmt.Println(a, b)
}
// 无参无返回值函数
func printInfo() {
fmt.Println("printInfo... ")
}
// 有一个参数的函数
func myPrint(msg string) {
fmt.Println(msg)
}
// 有两个参数的函数
// 有一个返回值的函数
func sub(a, b int) int {
c := a + b
return c
}
// 以多个返回值的函数
func swap(a, b int) (int, int) {
return b, a
}
8.1. 形参与实参
- 形式参数:定义函数时,用于接收外部传入数据的参数,就是形式參数。
- 实际参数:调用函数时,传给形参的实际数据叫做实际参数。
形参与实参要一一对应:顺序,个数,类型。
8.2. 可变参数
一个函数的参数类型确定,但个数不确定,就可以使用可变参数。
func myFunc(arg ... int) {}
// arg ... int 表示接收不定数量的参数,参数类型都是int
- 如果一个函数的参数是可变参数,同时还有其他的参数,可变参数要放在列表的最后。
- 一个函数的参数列表中最多只能有一个可变参数。
8.3. 参数传递
按照数据的存储特点来分:
值类型的数据:操作的是数据本身。int、string、bool、float64、array...
package main import "fmt" func main() { // 定义一个数组 arr1 := [4]int{1, 2, 3, 4} fmt.Println("arr1 默认数据", arr1) update(arr1) fmt.Println("arr1 调用函数后的数据", arr1) } func update(arr2 [4]int) { fmt.Println("arr2 接收数据", arr2) arr2[0] = 10 fmt.Println("arr2 修改后数据", arr2) } // arr1 默认数据 [1 2 3 4] // arr2 接收数据 [1 2 3 4] // arr2 修改后数据 [10 2 3 4] // arr1 调用函数后的数据 [1 2 3 4]
引用类型的数据:操作的是数据的地址。slice、map、chan...
package main import "fmt" // 引用传递 func main() { // 切片,可以扩容的数组 s1 := []int{1, 2, 3, 4} fmt.Println("默认的数据", s1) update2(s1) fmt.Println("调用后的数据", s1) } func update2(s2 []int) { fmt.Println("传递的数据", s2) s2[0] = 100 fmt.Println("修改后的数据", s2) } // 默认的数据 [1 2 3 4] // 传递的数据 [1 2 3 4] // 修改后的数据 [100 2 3 4] // 调用后的数据 [100 2 3 4]
8.4. 变量的作用域
作用域:变量可以使用的范围。
局部变量:函数内部定义的变量,叫做局部变量。
// 在if内的局部变量a // if 局部变量; 条件 if a := 1; true { fmt.Println(a) }
全局变量:函数外部定义的变量,叫做全局变量。
8.5. defer
defer:推迟、延迟
在Go语言中,使用defer关键字来延迟一个函数或方法的执行。
package main
import "fmt"
func main() {
f("1")
fmt.Println("2")
defer f("3") // 会被延迟到最后执行
fmt.Println("4")
}
func f(s string) {
fmt.Println(s)
}
// 1
// 2
// 4
// 3
defer函数或者方法:一个函数或方法的执行被延迟了。
- 可以在函数中添加多个defer语句,当函数执行到最后时,这些defer语句会按照逆序执行,最后该函数返回。当在进行一些打开资源操作时,遇到错误需要提前返回,在返回前需要关闭相应的资源,否则可能造成资源泄露等问题。
- 如果有很多调用defer,那么defer采用后进先出(栈)的模式。
package main
import "fmt"
func main() {
a := 10
fmt.Println("a=", a)
defer f(a) // 在执行的时候,参数已经传递到函数中,只是到最后才执行这个函数。
a++
fmt.Println("end a=", a)
}
func f(s int) {
fmt.Println("函数里面的a=", s)
}
// a= 10
// end a= 11
// 函数里面的a= 10
defer的用法:
对象
.close()
临时文件的删除。func op() { 文件.open() defer 文件.close() // 读或写的操作 }
Go语言中关于异常的处理,使用
panic()
和recover()
- panic函数用于引发恐慌,导致程序中断运行。
- recover函数用于恢复程序的执行,语法上要求必须在defer中执行。
8.6. 函数的数据类型
函数也是一种数据类型。
函数的数据类型: func (参数类型) (返回值类型
package main
import "fmt"
func main() {
a := 10
fmt.Printf("%T\n", a)
b := [4]int{1, 2, 3, 4}
fmt.Printf("%T\n", b)
c := []int{1, 2, 3, 4}
fmt.Printf("%T\n", c)
d := make(map[int]string)
fmt.Printf("%T\n", d)
// 不能加括号,否则就是调用
// 不加括号的情况下可以看作是个变量
fmt.Printf("%T\n", f1)
fmt.Printf("%T\n", f2)
// 函数本身也是一种数据类型,可以进行赋值,赋的值是地址,且赋值后可以作为函数被调用
var f5 (func(int) int) = f2
fmt.Printf("%T\n", f5)
}
func f1() {
}
func f2(a int) int {
return 0
}
// int
// [4]int
// []int
// map[int]string
// func()
// func(int) int
// func(int) int
8.7. 匿名函数
匿名函数就是没有名字的函数
package main
import "fmt"
func main() {
f3 := func() {
fmt.Println("我是一个匿名函数")
}
f3()
// 简化
// 匿名函数,函数体后增加一个()执行,通常只能执行一次
func() {
fmt.Println("我是一个匿名函数")
}()
// 定义带参数的匿名函数
func(a, b int) {
fmt.Println(a, b)
}(1, 2)
// 定义带返回值的匿名函数
r1 := func(a, b int) int {
return a + b
}(10, 20)
fmt.Println(r1)
}
Go语言是支持函数式编程的。
- 将匿名函数作为另外一个函数的参数,回调函数。
- 将匿名函数作为另外一个函数的返回值,可以形成闭包结构。
8.8. 回调函数
可以将一个函数作为另外一个函数的参数。
fun1(), fun2()
将fun1函数作为fun2这个函数的参数,则:
fun2函数为高阶函数,接收了一个函数作为参数的函数。
fun1函数叫做回调函数,作为另外一个函数的参数。
package main
import "fmt"
func main() {
// 调用函数
r1 := add(1, 2)
fmt.Println(r1)
// 高阶函数,add函数作为参数传递给oper函数
r2 := oper(10, 20, add)
fmt.Println(r2)
r3 := oper(10, 20, sub)
fmt.Println(r3)
// 匿名函数
fun1 := func(a, b int) int {
return a * b
}
r4 := oper(10, 20, fun1)
fmt.Println(r4)
r5 := oper(50, 20, func(a, b int) int {
if b == 0 {
fmt.Println("除数不能为0")
return 0
}
return a / b
})
fmt.Println(r5)
}
func add(a, b int) int {
return a + b
}
func sub(a, b int) int {
return a - b
}
func oper(a, b int, f func(int, int) int) int {
return f(a, b)
}
// 3
// 30
// -10
// 200
// 2
9. 闭包
一个外层函数中,有内层函数,该内层函数中,会操作外层函数的局部变量,并且该外层函数的返回值就是这个内层函数。
这个内层函数和外层函数的局部变量,统称为闭包结构
局部变量的生命周期就会发生改变,正常的局部变量会随着函数的调用而创建,随着函数的结束而销毁。
但是闭包结构中的外层函数的局部变量并不会随着外层函数的结束而销毁,因为内层函数还在继续使用。
package main
import "fmt"
func main() {
// 调用此方法时会拿到内部函数变量,同时会有外部函数的局部变量
// 调用同一个方法时,则会操作同一个变量
r1 := increment()
fmt.Println(r1)
v1 := r1()
fmt.Println(v1)
v2 := r1()
fmt.Println(v2)
fmt.Println(r1())
fmt.Println(r1())
// 再调用一次外部方法,会产生新的函数变量和外部函数的局部变量
r2 := increment()
fmt.Println(r2())
fmt.Println(r1())
}
func increment() func() int {
// 局部变量i
i := 0
// 内层函数只定义并还未执行
fun := func() int {
i++
return i
}
return fun
}
// 1
// 2
// 3
// 4
// 1
// 5
10. 断言
类型断言x.(T)
其实就是运桨如T是否实现了X接口,如果实现了就把X接口类型具体化为T类型。
package main
import "fmt"
func main() {
str := []string{"hello", "world"}
printArray(str)
}
func printArray(arr interface{}) {
for _, v := range arr.([]string) {
fmt.Println(v)
}
}
11. 泛型
package main
import "fmt"
func main() {
str := []string{"hello", "world"}
printArray(str)
i := []int{1, 2, 3}
printArray(i)
}
func printArray[T string | int](arr []T) {
for _, v := range arr {
fmt.Println(v)
}
}
内置的泛型类型any和comparable
any
:表示go里面所有的内置基本类型,等价于interface{}
.comparable
:表示go里面所有内置的可比较类型。int
,uint
,float
,bool
,struct
,指针
等一切可以比较的类型。
func printArray[T any](arr []T) {
for _, v := range arr {
fmt.Println(v)
}
}
定义类型形参
传统
type s1 []int
var a s1 = []int{1,2,3}
var b s2 = []float32{1.0} // 错误,s1的底层类型是[]int,浮点类型的切片无法赋值
类型形参:只定义一个类型就能代表多个类型。
type Slice[T int|float32|float64] []T
type MyMap[KEY int | string, VALUE any] map[KEY]VALUE
- T:占位符
int|float32|float64
类型约束(Type constraint),表示T可拥有的实参。T int|float32|float64
类型形参列表(Type parameter list)。
func main() {
// Slice[T]
type Slice[T int | float32 | float64] []T
var a Slice[int] = []int{1, 2, 3}
fmt.Print(a)
fmt.Printf("%T\n", a)
var b Slice[float32] = []float32{1, 2, 3}
fmt.Print(b)
fmt.Printf("%T\n", b)
var c Slice[float64] = []float64{1, 2, 3}
fmt.Print(c)
fmt.Printf("%T\n", c)
}
func main() {
type MyMap[KEY int | string, VALUE any] map[KEY]VALUE
var m1 MyMap[string, float64] = map[string]float64{
"go": 9.9,
"java": 9.0,
}
fmt.Println(m1)
}
泛型函数
package main
import "fmt"
type MySlice[T int | float64] []T
// 相当于在MySlice中定义了sum方法
func (s MySlice[T]) Sum() T {
var sum T
for _, v := range s {
sum += v
}
return sum
}
func main() {
var s MySlice[int] = []int{1, 2, 3, 4}
fmt.Println(s.Sum())
var s1 MySlice[float64] = []float64{1.0, 2.6, 3, 4}
fmt.Println(s1.Sum())
}
package main
import "fmt"
func main() {
fmt.Println(Add[int](1, 2))
// 如果类型可以被自动推断,则可以省略
fmt.Println(Add(1, 2))
}
//func Add(a int, b int) {
// return a + b
//}
func Add[T int | float64](a T, b T) T {
return a + b
}
自定义泛型类型
// 像接口一样声明,叫做泛型的约束,自定义约束
type int8AAA int8
type MyInt interface {
int | int8 | int16 | int32 | int64 | int8AAA
}
// T的声明类型为MyInt
func GetMaxNum[T MyInt](a,b T) T {
if a > b {
return a
}
return b
}
func main() {
var a int8AAA = 10
var b int8AAA = 20
fmt.Println(GetMaxNum(a, b))
fmt.Println(GetMaxNum[int8AAA](a, b))
}
type int8AAA int8
type MyInt interface {
// 加~自动适配此类型的引申类型
int | ~int8 | int16 | int32 | int64
}
func GetMaxNum[T MyInt](a,b T) T {
if a > b {
return a
}
return b
}
func main() {
var a int8AAA = 10
var b int8AAA = 20
fmt.Println(GetMaxNum(a, b))
fmt.Println(GetMaxNum[int8AAA](a, b))
}