Go 语言的优点:
-
高性能、高并发
-
语法简单
-
拥有丰富的标准库
-
快速编译
-
跨平台
-
垃圾回收机制
执行流程:
go run:在底层还是会将.go文件编译成二进制文件再执行,这步只是被隐藏起来了
两种执行流程的区别:
1.可执行文件可以在任意一台机器上运行,尽管没有go的开发环境环境也可以(这就是为什么原本的.go文件很小,生成可执行文件后很大的原因,里面包含了程序运行依赖的库文件)
2.如果是直接用go run 运行的,那么在另外一台机器上运行需要go的开发环境。
Go程序开发的注意事项:
-
main函数是程序的执行入口
-
Go程序的执行入口时main函数
-
Go语言时严格区分大小写
-
每条语句不需要有;结尾,程序默认在最后加一个分号。(加上不报错)
-
一行只能写一条语句
-
Go语言定义的变量或者import的包如果没有用到则编译不通过
编码规范:
注释:
-
包注释:
-
每个包都应该有一个包注释,一个位于package子句之前行注释
-
包注释应该包含下面基本信息
// @Description 描述包的作用 // @Author 创建人 // @Update 创建时间
-
-
结构(接口)注释:
该注释对结构进行简要介绍,放在结构体定义的前一行,格式为: 结构体名, 结构体说明。同时结构体内的每个成员变量都要有说明,该说明放在成员变量的后面(注意对齐),实例如下:
// User , 用户对象,定义了用户的基础信息 type User struct{ Username string // 用户名 Email string // 邮箱 }
-
函数(方法)注释:
每个函数,或者方法(结构体或者接口下的函数称为方法)都应该有注释说明,包括函数描述,参数描述,返回值描述
-
代码逻辑注释:
对于一些关键位置的代码逻辑,或者局部较为复杂的逻辑,需要有相应的逻辑说明,方便其他开发者阅读该段代码
-
总结:
代码时最好的注释
注释应该提供代码未表达出的上下文的意思
正确的缩进和空白:
-
使用tab操作,默认整体向右移动,shift+tab整体向左移动
-
缩进直接使用 gofmt 工具格式化即可(gofmt 是使用 tab 缩进的);
-
Goland开发工具,可以直接使用快捷键:ctrl+alt+L,即可。
命名规范:
命名是代码规范中很重要的一部分,统一的命名规则有利于提高的代码的可读性,好的命名仅仅通过命名就可以获取到足够多的信息。
Go在命名时以字母a到Z或a到Z或下划线开头,后面跟着零或更多的字母、下划线和数字(0到9)。Go不允许在命名时中使用@、$和%等标点符号。
-
当命名以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像Java语言中的 public);
-
命名如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像Java语言中的 private )
1、包命名:package
保持package的名字和目录保持一致,尽量采取有意义的包名,简短,有意义,尽量和标准库不要冲突。包名应该为小写单词,不要使用下划线或者混合大小写。
package demo package main
2、 文件命名
尽量采取有意义的文件名,简短,有意义,应该为小写单词,使用下划线分隔各个单词。
my_test.go
3、 结构体命名
-
采用驼峰命名法,首字母根据访问控制大写或者小写
-
struct 申明和初始化格式采用多行,例如下面:
// 多行申明 type User struct{ Username string Email string } // 多行初始化 u := User{ Username: "astaxie", Email: "astaxie@gmail.com", }
4、 接口命名
-
命名规则基本和上面的结构体类型
-
单个函数的结构名以 “er” 作为后缀
type Reader interface { Read(p []byte) (n int, err error) }
5、变量命名
-
和结构体类似,变量名称一般遵循驼峰法,首字母根据访问控制原则大写或者小写
6、常量命名
常量均需使用全部大写字母组成,并使用下划线分词
const APP_VER = "1.0"
如果是枚举类型的常量,需要先创建相应类型:
type Scheme string const ( HTTP Scheme = "http" HTTPS Scheme = "https" )
常量可以用len(), cap(), unsafe.Sizeof()函数计算表达式的值。常量表达式中,函数必须是内置函数,否则编译不过:
package main import "unsafe" const ( a = "abc" b = len(a) c = unsafe.Sizeof(a) ) func main(){ println(a, b, c) }
iota
iota,特殊常量,可以认为是一个可以被编译器修改的常量。
iota 在 const关键字出现时将被重置为 0(const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。
iota 可以被用作枚举值:
package main import "fmt" func main() { const ( a = iota //0 b //1 c //2 d = "ha" //独立值,iota += 1 e //"ha" iota += 1 f = 100 //iota +=1 g //100 iota +=1 h = iota //7,恢复计数 i //8 ) fmt.Println(a,b,c,d,e,f,g,h,i) }
//输出结果 0 1 2 ha ha 100 100 7 8
小结:
-
核心目标是降低阅读理解代码的成本
-
重点考虑上下文信息,设计简洁清晰的名称
控制流程:
-
避免嵌套,保持正常流程清晰
-
尽量保持正常代码路径为最小缩进
-
优先处理错误情况/特殊情况,尽早返回或继续循环来减少嵌套
错误和异常处理:
error尽可能提供简明的上下文信息链,方便定位问题,panic用于真正异常的情况,recover生效范围,在当前goroutine的被defer的函数中生效。错误处理的原则就是不能丢弃任何有返回err的调用,不要使用 _ 丢弃,必须全部处理。接收到错误,要么返回err,或者使用log记录下来尽早return:一旦有错误发生,马上返回。
变量:
//定义变量 //第一种:定义变量i,不赋值默认为0,同时在内存中是一块地址 var i int //第二种:根据值自行判断变量类型(类型推导) var num = 1.11 //第三种:省略var,:=左侧的变量不能是已经声明过的,不然编译会报错 name := "wjx" //多变量声明(与单变量声明类似) n1, name, n2 := 100, "tom",111
全局变量:
全局变量定义在main函数外面,而局部变量定义在main函数里面。
package main import "fmt" //定义全局变量 var n1 = 10 var n2 = 20 var name = "jack" //也可以这样一次性定义 var ( n3 = 30 n4 = 40 name1 = "mike" ) func main() { fmt.Println("success") }
PS:全局变量是允许声明但不使用的
数据类型:
-
基本设计类型:
-
数值型:
-
整数类型([有符号]int,int8,int16,int32,int64,rune
[无符号]uint,uint8,uint16,
uint32,uint64,byte)
-
浮点型(float32,float64,complex64,complex128)
-
-
字符型:使用byte保存单个字母符号
-
布尔型:值只可以是常量 true 或者 false
-
字符串型:字符串的字节使用 UTF-8 编码标识 Unicode 文本
-
-
派生/复杂数据类型:
-
指针:
-
数组:
-
结构体:
-
管道:
-
函数:
-
切片:
-
接口:
-
map:
-
查看数据类型和占用字节大小:
//fmt.Printf()可以用于做格式化的输出 fmt.Printf("xx 的 类型 %T", xx) //查看占用字节大小(是unsafe包的一个函数) fmt.Printf("xx 占用的字节数是%d",xx,unsafe.Sizeof(xx))
Go语言结构:
package main import "fmt" func main() { /* 这是我的第一个简单的程序 */ fmt.Println("Hello, World!") }
-
源文件中,第一行非注释代码必须指名这个文件是属于哪个包。
-
package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。
-
import表示是引入包,并且在后面的程序中用到该包的函数或其他元素,如果没使用到则会报错。
-
func main() 是程序开始执行的函数。main 函数是每一个可执行程序所必须包含的
-
当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected )。
PS:"{" 不能单独放在一行,否则会报错
Go语言条件语句:
select语句:
select 语句只能用于通道操作,每个 case 必须是一个通道操作,要么是发送要么是接收。
select 语句会监听所有指定的通道上的操作,一旦其中一个通道准备好就会执行相应的代码块。
如果多个通道都准备好,那么 select 语句会随机选择一个通道执行。如果所有通道都没有准备好,那么执行 default 块中的代码。
-
语法:
select { case <- channel1: // 执行的代码 case value := <- channel2: // 执行的代码 case channel3 <- value: // 执行的代码 // 你可以定义任意数量的 case default: // 所有通道都没有准备好,执行的代码 }
-
每个 case 都必须是一个通道
-
所有被发送的表达式都会被求值
-
如果任意某个通道可以进行,它就执行,其他被忽略。
-
如果有多个 case 都可以运行,select 会随机公平地选出一个执行,其他不会执行。 否则:
-
如果有 default 子句,则执行该语句。
-
如果没有 default 子句,select 将阻塞,直到某个通道可以运行;Go 不会重新对 channel 或值进行求值。
-
-
-
实例
package main import ( "fmt" "time" ) func main() { c1 := make(chan string) c2 := make(chan string) go func() { time.Sleep(1 * time.Second) c1 <- "one" }() go func() { time.Sleep(2 * time.Second) c2 <- "two" }() for i := 0; i < 2; i++ { select { case msg1 := <-c1: fmt.Println("received", msg1) case msg2 := <-c2: fmt.Println("received", msg2) } } }
-
c1 := make(chan string) 创建管道,管道名为c1
-
c1 <- "one" 将内容输送进管道
-
msg1 := <-c1 将内容输出到管道并复制给msg1
可以理解为:msg1 := <-c1 <- "one"
-
go func() go 是一个关键字,它告诉 Go 运行时在一个新的 goroutine 中运行该函数。func() 则定义了一个匿名函数,它没有名称,只有函数体1。
当程序执行到 go func() 语句时,它会立即创建一个新的 goroutine 并在其中执行这个匿名函数。这样,该函数的执行就与主程序流分离开来,可以并发地运行
-
if语句
-
语法
if 布尔表达式 { /* 在布尔表达式为 true 时执行 */ }
表达式不需要像别的语言一样要()
switch语句
-
语法
switch var1 { case val1: ... case val2: ... default: ... }
支持多条件匹配
switch{ case 1,2,3,4: default: }
不同的 case 之间不使用 break 分隔,默认只会执行一个 case。
如果想要执行多个 case,需要使用 fallthrough 关键字,也可用 break 终止。
-
fallthrough
用法:
package main import "fmt" func main() { switch { case false: fmt.Println("1、case 条件语句为 false") fallthrough case true: fmt.Println("2、case 条件语句为 true") fallthrough case false: fmt.Println("3、case 条件语句为 false") fallthrough case true: fmt.Println("4、case 条件语句为 true") case false: fmt.Println("5、case 条件语句为 false") fallthrough default: fmt.Println("6、默认 case") } }
switch 从第一个判断表达式为 true 的 case 开始执行,如果 case 带有 fallthrough,程序会继续执行下一条 case,且它不会去判断下一个 case 的表达式是否为 true。
循环语句
for循环
-
语法:
-
for init; condition; post { }
实例:
package main import "fmt" func main() { sum := 0 for i := 0; i <= 10; i++ { sum += i } fmt.Println(sum) }
-
for condition { }
实例:
package main import "fmt" func main() { sum := 1 // 这样写也可以,更像 While 语句形式 for sum <= 10{ sum += sum } fmt.Println(sum) }
-
-
For-each range 循环
package main import "fmt" func main() { strings := []string{"google", "runoob"} for i, s := range strings { fmt.Println(i, s) } numbers := [6]int{1, 2, 3, 5} for i,x:= range numbers { fmt.Printf("第 %d 位 x 的值 = %d\n", i,x) } }
for 循环的 range 格式可以省略 key 和 value,如下实例:
package main import "fmt" func main() { map1 := make(map[int]float32) map1[1] = 1.0 map1[2] = 2.0 map1[3] = 3.0 map1[4] = 4.0 // 读取 key 和 value for key, value := range map1 { fmt.Printf("key is: %d - value is: %f\n", key, value) } // 读取 key for key := range map1 { fmt.Printf("key is: %d\n", key) } // 读取 value for _, value := range map1 { fmt.Printf("value is: %f\n", value) } }
循环控制语句
-
break 种植当前循环,跳出循环语句
-
continue 不执行当前循环的剩余语句,直接开始下一轮循环
-
goto label ... label: statement
其中,label 是一个标签,它可以是除关键字以外的任何有效的 Go 语句。当程序遇到 goto 语句时,它会立即跳转到指定的标签处,并从那里继续执行。
需要注意的是,使用 goto 语句通常不被推荐,因为它会使程序的控制流变得难以理解和修改。任何使用 goto 的程序都可以使用其他结构来重写。
无限循环
package main import "fmt" func main() { for true { fmt.Printf("这是无限循环。\n"); } }
语言函数
函数定义:
func 函数名( [参数列表] ) [返回值] { 函数体 }
函数闭包:
例子:
package main import "fmt" func getSequence() func() int { i:=0 return func() int { i+=1 return i } } func main(){ /* nextNumber 为一个函数,函数 i 为 0 */ nextNumber := getSequence() /* 调用 nextNumber 函数,i 变量自增 1 并返回 */ fmt.Println(nextNumber()) fmt.Println(nextNumber()) fmt.Println(nextNumber()) /* 创建新的函数 nextNumber1,并查看结果 */ nextNumber1 := getSequence() fmt.Println(nextNumber1()) fmt.Println(nextNumber1()) }
闭包体现在getSequence函数中。这个函数返回了一个匿名函数,该匿名函数捕获了getSequence函数中定义的变量i。这意味着即使getSequence函数执行完毕,返回的匿名函数仍然可以访问变量i。这个匿名函数在每次调用时都会将变量i的值增加1,并返回增加后的值。由于这个匿名函数捕获了变量i,因此它可以在每次调用时访问和修改变量i的值。
函数方法:
定义:
func (variable_name variable_data_type) function_name() [return_type]{ /* 函数体*/ }
例子:
package main import ( "fmt" ) /* 定义结构体 */ type Circle struct { radius float64 } func main() { var c1 Circle c1.radius = 10.00 fmt.Println("圆的面积 = ", c1.getArea()) } //该 method 属于 Circle 类型对象中的方法 func (c Circle) getArea() float64 { //c.radius 即为 Circle 类型对象中的属性 return 3.14 * c.radius * c.radius }
(c Circle)
是一个方法接收器,它指定了 getArea
方法属于 Circle
类型。这意味着 getArea
方法可以被 Circle
类型的变量调用。这里的c只是接受的变量名,可以任意更改。
数组:
定义格式:
var arrayName [size]dataType
初始化:
//这个方法默认值是为0 var numbers [5]int
var numbers = [5]int{1, 2, 3, 4, 5}
也可直接定义加初始化:
numbers := [5]int{1, 2, 3, 4, 5}
数组的长度不同,则两者不兼容也就是说 [5]int 和 [10]int 是不同的类型。
如果数组的长度不确定,可以使用 ... 代替数组的长度,编译器会根据元素个数自行推断数组的长度:
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0} //或 balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
数组长度确定,可以通过指定下标来初始化元素:
// 将索引为 1 和 3 的元素初始化 balance := [5]float32{1:2.0,3:7.0}
指针
指向内存地址的变量。
定义:
var var_name *var-type
去变量的地址时,在变量前面加上&即可。
想要获取该地址的值,在指针变量前面加上*即可。
指针数组
定义:
var var_name [MAX]*var-type;
指针的指针:
定义:
var ptr **int;
例子:
package main import "fmt" func main() { var a int var ptr *int var pptr **int a = 3000 /* 指针 ptr 地址 */ ptr = &a /* 指向指针 ptr 地址 */ pptr = &ptr /* 获取 pptr 的值 */ fmt.Printf("变量 a = %d\n", a ) fmt.Printf("指针变量 *ptr = %d\n", *ptr ) fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr) } //输出结果: //变量 a = 3000 //指针变量 *ptr = 3000 //指向指针的指针变量 **pptr = 3000
切片
简单理解就是没有定义长度的数组,也可以理解为动态长度数组
定义:
var identifier []type
也可以使用make来定义:
var slice1 []type = make([]type, len) //也可以简写为 slice1 := make([]type, len) //也可以指定容量 make([]T, length, capacity)
容量指的是底层数组的最大长度。
初始化:
s :=[] int {1,2,3 }
数组引用:
s := arr[startIndex:endIndex]
s[:2]:表示从0到1的值,不包括2.
Map
定义:
/* 使用 make 函数 */ map_variable := make(map[KeyType]ValueType, initialCapacity)
其中 KeyType 是键的类型,ValueType 是值的类型,initialCapacity 是可选的参数,用于指定 Map 的初始容量。
直接在创建时初始化值:
m := map[string]int{ "apple": 1, "banana": 2, "orange": 3, }
增加键值对:
// 没有该键则增加,有则修改键值对 m["apple"] = 5
遍历:
for k, v := range m { fmt.Printf("key=%s, value=%d\n", k, v) }
删除:
delete(m, "banana")
获取长度:
len := len(m)
类型转换:
语法:
type_name(expression)
type_name 为转换类型,expression 为被转换的值。
数值类型转换
var a int = 10 var b float64 = float64(a)
go 不支持隐式转换类型,就是比如int32->int64 会报错
字符串类型转换
var str string = "10" var num int var num1 float64 //字符串转整数 num, _ = strconv.Atoi(str) //字符串转浮点 num1, err := strconv.ParseFloat(str, 64) //整数转字符串 str, _ = strconv.Itoa(num)
strconv.Atoi 函数返回两个值,第一个是转换后的整型值,第二个是可能发生的错误,我们可以使用空白标识符 _ 来忽略这个错误
接口
它定义了一组方法的集合,但不提供这些方法的具体实现。接口类型的变量可以保存任何实现了该接口的类型的值。
定义:
type Shape interface { Area() float64 Perimeter() float64 }
例子:
package main import "fmt" type Shape interface { area() float64 } type Rectangle struct { width float64 height float64 } func (r Rectangle) area() float64 { return r.width * r.height } type Circle struct { radius float64 } func (c Circle) area() float64 { return 3.14 * c.radius * c.radius } func main() { var s Shape s = Rectangle{width: 10, height: 5} fmt.Printf("矩形面积: %f\n", s.area()) s = Circle{radius: 3} fmt.Printf("圆形面积: %f\n", s.area()) }
错误处理:
内置错误接口:
type error interface { Error() string }
并发:
只需要通过 go 关键字来开启 goroutine 即可。
语法:
go 函数名( 参数列表 )
channel
c1 := make(chan string) 创建管道,管道名为c1
c1 <- "one" 将内容输送进管道
msg1 := <-c1 将内容输出到管道并复制给msg1
可以理解为:msg1 := <-c1 <- "one"
通道缓冲区
ch := make(chan int, 100)
带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。
不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。
注意:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。
遍历通道:
v, ok := <-ch
如果通道接收不到数据后 ok 就为 false,这时通道就可以使用 close() 函数来关闭。