tip: 无论是变量、方法还是struct的访问权限控制都是通过命名控制的,命名的首字母是大写就相当于java中的public,小写的话就是private,(private只有本包可以访问)
1 go的变量声明
普通变量
特点: 变量类型后置
方式1:
var num int = 1
方式2:
num := 1 // := 会帮我们自动匹配数据类型(最常用)
方式3:
var num = 1
常量
还是通过const关键字声明(和c是一样的)
const (
//声明常数列表
ZHANGSAN = 1
LISI = 2
....
)
并且我们可以使用iota关键字进行简写
const (
ZHANGSAN = iota + 1 //iota默认是0,每次被使用后都会+1
LiSI //等同于LISI = iota + 1, 但是公式没有发生变化所以可以省略
)
常量使用:
fmt.println("ZHANGSNA = ", ZHANGSAN)
2 go的函数/方法声明
特点: func (绑定struct名 类型) 函数名(形参名 形参类型, …) 返回类型 {函数体}
func print(s string) string {
....
}
go当中的方法是可以有多个返回值的:
func play() (string, string) {
return zhangsan, lisi
}
//多返回值接收
name1, name2 := play()
3.对象
声明
特点: go语言的对象是一种组合概念,由结构体+一系列方法构成
type关键字:定义自定义类型
type person struct {
name string
age int
}
func (p *person) setName(name string) {
p.name = name
}
上述代码就等同于java中的如下代码:
private class person {
private String name;
private int age;
private void setName(String name) {
this.name = name;
}
}
"(p *person)"代表这个方法绑定person这个struct
为什么要使用"*person"?
因为go中的对象(struct)是值类型(也就是java中的基本类型),因此不传入指针的话修改也不生效(需要一点c的指针基础)
类继承
type human struct {
name string
age int
}
type man struct {
human // 就代表man继承了human对象
sex int
}
//man的创建
man1 := man{human{"zhangsan", 18}, 1}
4 init方法
特点: 导包时执行的初始化方法
package p1
import "fmt"
func init() {
fmt.println("p1, init")
}
如果我们引入这个包,那么就会执行p1的init方法
5 指针
go中的指针和c是一样的
6 defer关键字
特点: 在defer所在的代码块执行完后执行,类似java的finally或c++的析构函数
使用场景: 文件关闭、连接关闭、资源释放等
func print() {
defer fmt.println("2..")
fmt.println("1..")
}
输出:先1后2
//defer可以有多个,通过栈结构存储
func print() {
defer fmt.println("2..") //2进栈
defer fmt.println("3..") //3进栈
fmt.println("1..")
}
输出:先1后3再2
tip:defer与return的执行顺序,因为defer是在所在方法的生命周期结束后才会执行
func deferCall() {
fmt.println("defer...")
}
func returnCall() {
fmt.println("return...")
}
func deferAndReturn() {
defer deferCall()
return returnCall()
}
func main() {
deferAndReturn()
}
输出: return...
defer...
7 数组与动态数组
声明
固定数组(常用)
arr := [3]int{1,2,3}
动态数组(也叫切片slice)
arr := []int{1, 2, 3} 或 //虽然我们只添加了三个初始值,但是我们还可以往后继续添加
arr1 := make([]int, 3) //开辟大小为3的数组,但是没有设置初始值
arr2 := make([]int, 3, 5) //开辟一个大小为3,容量为5的切片(arr[3]和arr[4]实际还不可以访问)
容量的概念就是我们声明了这个空间但是我没创建(也就是我告诉你这有但是现在还没有),大小就是现在实际有的
切片扩容:当容量不够时,会自动扩容为当前容量的2倍,没指定容量的话就是切片大小
对动态数组的操作与python中的切片操作类似。
tip:
go当中的固定数组是一种直接类型,不像是java中的引用类型,所以[3]int 和 [4]int不是同一类型的,所以在使用固定数组作为方法形参的时候如果想要改变内容应该传入指针。
而动态数组是引用类型,type(arr)得到的是[]int.
遍历
arr := []int{1,2,3,4}
方式1:
for index, value := range arr {
fmt.println("下标是:", index)
fmt.println("内容是:", value)
}
方式2:
for i := 1, i < len(arr), i++ {
fmt.println(arr[i])
}
8 map
声明
var myMap map[String]string //[]内是key类型,外是value类型
//开辟空间
myMap := make(map[string]string, 10)
//有初始值创建
myMap := map[string]string{
"one": "zhangsan"
"two": "lisi"
}
使用
myMap := make(map[string]string)
//增
myMap["one"] = "zhangsan"
myMap["two"] = "lisi"
//删
delete(myMap, "one")
//改
myMap["two"] = "wangwu"
//查
value := myMap["two"]
// 遍历
for key, value := range myMap {
}
9 多态
特点: 实现接口
type animal interface {
show()
play()
}
// cat实现了animal接口, 是通过实现接口方法
type cat struct {
name string
}
func (c *cat) show() {
fmt.println("这是一只猫")
}
func (c *cat) play() {
fmt.println("猫在玩耍")
}
10.interface{}
特点: 通用数据类型,几乎所有类型都实现了interface{}
func call(this interface{}) {
fmt.println("this is ", this)
// 类型断言
value, res := this.(string) // 判断this是不是是tring类型,会返回两个数据一个是this本身一个是判断结果(bool)
}
type human struct {
name string
}
func main() {
human1 := human{"zhangsan"}
//call 方法可以接收任何类型
call(human1)
call(1)
call("lisi")
}
11 pair
任何变量都会有一个piar对变量描述,这个piar结构如下:<type, value>,type代表变量的类型,value代表变量存储的值。
type又分为static type(基本类型,如:int、char这些)和concrete type(引用类型,如:切片)
12 反射
go提供了reflect包实现反射功能,其中有两个核心方法TypeOf()和ValueOf(), 分别用来获取参数对象的类型与值。
反射基本使用
package main
import (
"fmt"
"reflect"
)
type User struct {
Name string
Age int
}
func (this User) Call() { //这里写*User会识别不到方法,原因在后面
fmt.Println("User is call...")
fmt.Printf("%v\n", this)
}
func main() {
user := User{"zhangsan", 18}
DoFiledAndMethod(user)
}
func DoFiledAndMethod(input interface{}) {
// 获取input信息
t := reflect.TypeOf(input)
fmt.Println("type:", t.Name())
v := reflect.ValueOf(input)
fmt.Println("value:", v)
// 遍历结构体的所有字段
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i).Interface()
fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
}
methodNum := t.NumMethod()
fmt.Printf("the User has %d methods\n", methodNum)
// 通过type调用方法
for i := 0; i < t.NumMethod(); i++ {
method := t.Method(i)
fmt.Printf("%s: %v\n", method.Name, method.Type)
}
}
Go 的方法集规则
Go 的方法集规则如下:
- 值类型(
T
) 的方法集包含所有接收者为T
的方法。 - 指针类型(
*T
) 的方法集包含所有接收者为T
和*T
的方法。
因此:
User
值类型的方法集:只有接收者为User
的方法。*User
指针类型的方法集:接收者为User
和*User
的方法。
13 标签
在 Go 语言中,标签(Tags) 是结构体字段的元数据,通常用于编码/解码(如 JSON、XML)、ORM 映射(如 GORM)、验证(如 validator
)等场景。标签以反引号 `` 包裹,格式为 key:“value”,多个标签用空格分隔。
package main
import (
"fmt"
"encoding/json"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
user := User{"zhangsan", 18}
// 结构体 -> json
jsonData, _ := json.Marshal(user)
fmt.Println(string(jsonData))
//json -> 结构体
myUser := User{}
err := json.Unmarshal(jsonData, &myUser)
if(err != nil) {
fmt.Println("err: ", err)
return
}
fmt.Printf("%v\n", myUser)
}
输出:
{"name":"zhangsan","age":18}
{zhangsan 18}
14 协程
协程(Goroutine) 是一种轻量级的并发执行单元,由 Go 运行时(runtime)管理,用于实现高并发编程。
go语言对于协程的设计是将原先线程中用户态部分分离出来了,这样不需要频繁的切换,而原线程中的内核态部分成为新的线程复杂管理协程。
协程结构:
GMP模型控制:
关键组件:
- G (Goroutine)
- 包含执行栈、程序计数器(PC)、状态(运行/阻塞)。
- 通过
go
关键字创建,轻量且可动态扩缩。
- P (Processor)
- 协程调度器,绑定到 M 上运行。
- 维护一个本地 Goroutine 队列(优先调度本地队列)。
- M (Machine)
- 操作系统线程,真正执行计算的单元。
- 由 P 分配任务,空闲时会被回收或休眠。
调度流程:
1. G 创建 → 放入 P 的本地队列(或全局队列)。//只用本地队列满会放入全局队列
2. M 获取 G → 从绑定的 P 的队列中取 G 执行。
3. G 阻塞(如 I/O)→ 该阻塞协程从本地队列移除,M解绑P,由M负责该阻塞协程,解绑后的P绑定一个新的线程,P可以正常工作去调度其他G。
4. G 就绪 → 被放回队列,等待 M 执行。
Goroutine 解决了线程的哪些痛点?
(1) 高创建和切换成本
- 线程问题:每个线程需要分配较大的栈(1MB+),创建和切换涉及内核态操作,开销大。
- Goroutine 方案:初始栈仅 2KB,由 Go 运行时在用户态调度,切换成本极低。
(2) 并发数量受限
- 线程问题:受限于内存和内核调度,通常只能创建几千个线程。
- Goroutine 方案:轻量级设计,单机可轻松支持百万级并发,还有协程调度器的存在支持多对多的调度关系。
(3) 共享内存导致的竞态问题
- 线程问题:多线程需通过锁(
mutex
)同步,易引发死锁、数据竞争。 - Goroutine 方案:通过 Channel 通信,避免显式锁,更安全。
(4) 阻塞导致线程浪费
- 线程问题:一个线程阻塞(如 I/O 操作)时,线程池中的线程被占用,影响整体吞吐量。
- Goroutine 方案:阻塞时,Go 运行时会自动挂起该 Goroutine,复用线程执行其他任务。
协程的使用
package main
import (
"fmt"
"time"
)
func print() {
i := 0
for {
i++
fmt.Printf("new Goroutine: i = %d\n", i)
time.Sleep(1 * time.Second)
}
}
func main() {
go print() // 创建协程执行print方法
i := 0
for {
i++
fmt.Printf("main Goroutine: i = %d\n", i)
time.Sleep(1 * time.Second)
}
}
// 也可以通过匿名调用的方式
func main() {
go func () {
i := 0
for {
i++
fmt.Printf("new Goroutine: i = %d\n", i)
time.Sleep(1 * time.Second)
}
}() // 括号中填入方法的参数,我这是无参就没填
i := 0
for {
i++
fmt.Printf("main Goroutine: i = %d\n", i)
time.Sleep(1 * time.Second)
}
}
//执行结果
main Goroutine: i = 1
new Goroutine: i = 1
new Goroutine: i = 2
main Goroutine: i = 2
main Goroutine: i = 3
new Goroutine: i = 3
new Goroutine: i = 4
main Goroutine: i = 4
15 channel
用于协程间通信的一种方式,分为无缓冲channel和有缓冲channel
无缓冲channel:channel中同一时间只能存在一个数据
package main
import (
"fmt"
"time"
)
// 1
func main() { // 主协程
c := make(chan int)
fmt.Println("main Goroutine try to read from channel")
num := <-c
fmt.Println(num)
go func (){ //子协程
c <- 1
fmt.Println("already write")
}()
}
// 2
func main() {
c := make(chan int)
go func (){
c <- 1
fmt.Println("already write")
}()
fmt.Println("main Goroutine not read from channel")
i := 0
for {
i++
fmt.Println(i)
time.Sleep(1 * time.Second)
}
}
输出:
// 1
fmt.Println("main Goroutine try to read from channel")
1
// 2
main Goroutine not read from channel
1
2
3
4
5
子协程向channel中写入数据后会在此阻塞直到数据被取走,同样如果主协程想要从channel中读取数据但目前channel中没有数据主协程也会阻塞等待。
有缓冲channel:其与阻塞队列的功能非常相似,允许存在多个数据在channel中,如果从空channel中获取元素或向满channel中发送元素则会触发阻塞。
// 声明有缓冲channel
ch := make(chan int 3) //创建缓冲大小为3的有缓冲channel
虽然channel和阻塞队列功能类似但他俩在实现上还是有很大的不同,感兴趣的可以去了解一下。
关闭channel:
channel一旦关闭就无法向channel中发送数据,但如果缓冲区内还有数据则还可以读取。