Golang基础

Golang 基础

基础

1. 基本属性

1
2
3
4
5
6
1. fmt.Println  打印并换行
2. fmt.Printf("Name: %s\n", name) // 字符串
fmt.Printf("Age: %d\n", age) // 整数
fmt.Printf("Height: %.2f\n", height) // 浮点数,保留2位小数
fmt.Printf("Type: %T\n", name) // 变量类型
3. fmt.Print - 基本打印 不会自动换行 多个参数之间没有空格分隔

2.声明变量的基础方法

image-20251105205418533

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import "fmt"

func main() {
// 无默认值
var a int
fmt.Println("a=", a)
// 有类型有默认值
var b int = 100
fmt.Println("b=", b)
// 无类型 默认值
var c = 100
fmt.Println("c=", c)
// 省略关键字var 局部变量
d := 100
fmt.Println("d=", d)

e, f := 100, "a"
fmt.Println("e=", e, "f=", f)

var (
g int = 1
h string = "world"
)
fmt.Println("g=", g, "h=", h)

}

3. 枚举iota

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
const (
RED = 10 * iota
GREEN
BLUE
)
const (
a = 10 * iota
b = iota
c = iota + 5
)
fmt.Println(GREEN)

fmt.Println(a, b, c)

}
iota: 一行加1 只能出现在const 里面

函数返回

1.常用函数调用 多返回值 命名返回变量

函数是值拷贝 如果需要引用传递 就使用指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import "fmt"

// 常规返回
func foo1(a int, b int) (int, int) {
fmt.Println(a)
fmt.Println(b)
a += 1
b += 1
return a + b, a
}

// 命名返回
func foo2(a1 int, a2 int) (r1, r2 int) {
r1 = a1 + 2
r2 = a2 + 2
return
}

func main() {
println(3333)
d, e := foo1(1, 2)
fmt.Println("d=", d, "e=", e)

f, g := foo2(1, 2)
fmt.Println("f=", f, "g=", g)
}

首字母大写对外开发
首字母小写就是私有的

2. import init 初始化流程

image-20251104210652757

​ import :

​ mod: go mode init [moduleName] 初始化go mod的名称

1
2
3
4
import _ "fmt"   // 别名 无法使用里面方法 但是会执行init方法
import aa "fmt" // 别名 aa调用
import . "fmt" // 将fmt里面的所有的方法导入进去 可以直接使用 本地的会优先级比较高

### 3. defer

image-20251105180926947

指针

Go 语言中的指针(Pointer) 是一个核心特性,用于直接操作内存地址,提升性能或实现引用语义。指针是一个变量,存储的是另一个变量的内存地址。通过指针,可以间接访问或修改原始变量的值

1.基础使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func add(a *int, b int) {
*a += b
}

func swap(a *int, b *int) {
*a, *b = *b, *a // 右边一次性求值了在复制给左边
}

func main() {
a := 1
p := &a
add(p, 1)
fmt.Println(a)
}

&取引用 *解引用

2. 指针解决啥问题

  • 避免大对象的拷贝

  • 允许函数直接修改原始变量

  • 为空的时候语义

slice 切片

实际就是动态数组,切片是 对底层数组的一个动态“窗口”或“视图”,它 不是数组本身,而是包含三个字段的结构体: 切片是引用类型

1
2
3
4
5
type slice struct {
ptr *T // 指向底层数组的指针
len int // 当前长度
cap int // 容量(从 ptr 开始到底层数组末尾的元素个数)
}
1
2
3
var slice1 []int
slice1 = append(slice1, 1, 2, 3, 4, 5)
fmt.Println(slice1)

切片共享底层数组切片的零值是 nil

1.创建切片

image-20251105210939116

2.切片注意事项

  • Slice 是对底层数组的引用 会导致连带修改

  • append 可能导致底层数组重新分配

  • nil slice 与空 slice 的区别

  • 切片操作越界 panic

  • 避免内存泄漏(大数组切小片)

  • range 遍历时的副本问题

  • Slice 不是并发安全的。多个 goroutine 同时读写同一个 slice 会导致数据竞争 –使用互斥锁或改用 channel或确保每个 goroutine 操作独立 slice

  • 预分配容量提升性能

Map

Map(映射) 是一种内置的、无序的键值对(key-value)集合,用于根据“键”快速查找、存储或删除对应的“值”。

1.nil map

1
2
var n map[string]int
n["one"] = 1 //非法的 未开辟空间 nil map 是未初始化未开辟空间 空map是初始化了开辟了底层空间的 map

2.map注意事项

  • map 不是并发安全的

  • **map 的 key 必须是可比较类型 **

  • 基本类型:int, string, bool

  • 指针、channel

  • 结构体(所有字段都可比较

  • 接口(动态值可比较)

  • 遍历顺序是随机的

  • 不要对 map 取地址,map 是引用类型,本身就是一个“句柄”,再取地址意义不大

  • map 在增长时会触发 rehash(重建哈希表),可能导致短暂延迟,如果知道大致容量,可预分配(但 make(map, hint)

Struct 结构体

结构体(struct) 是一种用户自定义的复合数据类型,用于将多个不同类型的字段(fields)组合在一起,表示一个“对象”或“实体”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import "fmt"
type Book struct {
title string
author string
}
func main() {

var b1 = Book{
title: "hello",
author: "world",
}
fmt.Println(b1)
}

1. 结构体方法(封装)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import "fmt"

type Person struct {
name string
age int
}

func (this Person) getName() string {
fmt.Println("this.name:", this.name)
return this.name
}

func (this Person) setName(name string) {
this.name = name
}

func main() {
p1 := Person{"张三", 18} // 省略字段名 必须要所有字段都提供值 安定义顺序 类型和数字
p1 = Person{name: "李四", age: 12}
p1.setName("李四")
fmt.Println(p1.getName())
}

2.继承(组合优于继承

​ 结构体“嵌入”到另一个结构体中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import "fmt"

// 基础结构体(类似“父类”)
type Animal struct {
Name string
}

// Animal 的方法
func (a Animal) Speak() {
fmt.Println("I am", a.Name)
}

// Dog “嵌入” Animal(不是继承!)
type Dog struct {
Animal // 匿名嵌入(关键!)
Breed string
}

func main() {
d := Dog{
Animal: Animal{Name: "Buddy"},
Breed: "Golden Retriever",
}

// 直接调用嵌入结构体的方法和字段
d.Speak() // 输出: I am Buddy
fmt.Println(d.Name) // 输出: Buddy(Name 被“提升”了)
fmt.Println(d.Breed) // 输出: Golden Retriever
}

3. 多态

同一个接口,多种实现;同一操作,作用于不同对象,产生不同行为

只要一个类型实现了接口的所有方法,它就“自动”实现了该接口,无需显式声明

go的多态通过接口实现 接口本质上是个指针,比如 doEat(interfaceAnimal)

doEat(cat) 执行的是cat的eat方法

doEat(dog) 执行的的是dog的eat方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package main
import "fmt"

type Person struct {
Name string
Age int
}

func (p Person) String() string {
return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}


// 继承
type Student struct {
Person
score int
}

func (s Student) printScore() {
fmt.Println(s.Name, "的分数:", s.score)
}

func (s Student) sayHello(){
fmt.Println("大家好,我是", s.Name,"同学")
}




type Teacher struct {
Person
salary int
}

func (t Teacher) printSalary() {
fmt.Println(t.Name, "的工资:", t.salary)
}

func (t Teacher) sayHello(){
fmt.Println("大家好,我是", t.Name,"老师")
}



func sayHello(p PersonAction) {
fmt.Println("大家好,我是", p)
}


type PersonAction interface {
sayHello()
}

func main() {
t :=Teacher{Person{"李老师", 18}, 5000}
t.printSalary()
d :=Student{Person{"张同学", 18}, 90}
d.printScore()

// 多态
sayHello(d)
sayHello(t)

}

4. 接口

*** 空接口万能类型*** interface {} 接口变量包含两个指针

第一个指针:指向类型信息(type descriptor)

第二个指针:指向实际存储的值(value)

1
2
3
4
5
6
7
8
9
10
11
package main

func main() {
var s string
s = "hello world"
var allType interface{}
allType = s
println(allType)
str, _ := allType.(string) // 判断接口里面的类型 是不是string
println(str)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

type Reader interface {
ReadBook()
}

type Writer interface {
WriteBook()
}

type Book struct {
Title string
}

// 混合接收者:一个值接收者,一个指针接收者
func (b Book) ReadBook() { // 值接收者
println("reading book: " + b.Title)
}

func (b *Book) WriteBook() { // 指针接收者
println("writing book: " + b.Title)
}

func main() {
b := Book{"Go"}

// 情况分析:
var r1 Reader = b // ✅ ReadBook 是值接收者
var r2 Reader = &b // ✅ ReadBook 是值接收者

// var w1 Writer = b // ❌ 编译错误!WriteBook 是指针接收者
var w2 Writer = &b // ✅ WriteBook 是指针接收者

r1.ReadBook()
r2.ReadBook()
w2.WriteBook()
}

// 通过值接受和指针接受

1
2
3
4
5
6
7
8
9
10
11
12
var r Reader = b  // 当:
// 1. 所有接口方法都是值接收者
// 2. 对象很小,复制开销可忽略
// 3. 希望保持不可变性
// 4. 不需要修改原始对象

var r Reader = &b // 当:
// 1. 有任何方法是指针接收者(必须用指针)
// 2. 对象很大,避免复制开销
// 3. 需要修改原始对象
// 4. 结构体可能为nil需要处理
// 5. 统一使用指针(推荐默认方式)

注意事项

  • 类名、属性名 、方法名 大小写是可以对其他包开放 小写只能当前包访问

反射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
"fmt"
"reflect"
)

type Person struct {
Name string `key:"desc" required:"true"` // 反射标签 类似java的注解
Age int `key:"ageDesc" required:"true"`
}

func (p *Person) Say() {
fmt.Println("hello world")
}

func reflectNum(input interface{}) {
type1 := reflect.TypeOf(input)
fmt.Println(type1)
fmt.Println(reflect.ValueOf(input))

// 获取方法
for i := 0; i < type1.NumField(); i++ {
field := type1.Field(i)
fmt.Println(field.Name, field.Type)
}
}
func main() {
p := Person{"张三", 18}
reflectNum(p)
}

结构体标签应用 json标签、orm映射

为啥不设计 结构体整体标签 ? 方法标签?

goroutine & channel

并行: 多核同时执行进程 多个任务真正同时执行。 一起处理

并发: 同一时间段内交替执行, 感觉是同时 但是实际不是一起执行

多进程 高内存占用 虚拟内存

一个线程: 用户空间–内核空间 用户线程(协程 co-routine) <==> 内核线程

CPU绑定内核线程, N个内核线程通过协程调度器轮训 绑定M个协程

Go 语言中的 GMP 模型 是其并发调度的核心机制,用于高效地管理和执行 Goroutine(Go 的轻量级线程)。GMP 是三个核心组件的缩写:

  • G(Goroutine)
    • 表示一个 Go 协程,即用户态的轻量级线程。
    • 由 Go 运行时(runtime)管理,创建成本极低(初始栈仅 2KB),可轻松创建成千上万个。
    • 每个 G 都包含自己的栈、指令指针、状态等。
  • M(Machine / OS Thread)
    • 对应一个操作系统线程(OS thread)。
    • 真正执行代码的实体,由操作系统调度。
    • M 必须绑定到一个 P 才能运行 G。
  • P(Processor)
    • 是 Go 调度器中的“逻辑处理器”,不是 CPU 核心,但数量通常等于 CPU 核数(可通过 GOMAXPROCS 设置)。
    • 每个 P 拥有一个本地 Goroutine 队列(runq),用于存放待执行的 G。
    • P 是 M 和 G 之间的桥梁:M 必须持有 P 才能执行 G。

GMP 调度流程简述:

  1. 当你用 go func() 启动一个协程时,会创建一个 G

  2. 这个 G 会被放入某个 P 的本地队列中。

  3. 一个 M(系统线程)绑定到这个 P,从 P 的队列中取出 G 并执行。

  4. 如果某个 G 发生

    阻塞

    (如 I/O、系统调用),Go 调度器会:

    • 将该 M 与 P 解绑;
    • 把 P 绑定到另一个空闲的 M 上,继续执行其他 G;
    • 原来的 M 在阻塞结束后,会尝试重新获取一个 P 来继续执行(或进入休眠)。
  5. P 还会定期从全局队列或其他 P 的队列中“偷取”(work-stealing)G,实现负载均衡。

worksteal M闲置后偷取另外一

handofff 阻塞后新开线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"fmt"
"time"
)

func task() {
for i := 0; i < 10000; i++ {
println("次线程", i)
time.Sleep(1 * time.Second)
}
}

func main() {
go task()
fmt.Print("主线程结束")
// i := 0
// for {
// println("主线程", i)
// time.Sleep(1 * time.Second)
// i++
// }
}

主线程结束后对应的goroutine也会结束

channel 通信

  • 基本操作 初始化chan 写入数据 for range读取数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"fmt"
"time"
)

func main() {
c := make(chan int, 3)
fmt.Println(len(c), cap(c))

// 无缓冲
go func() {
fmt.Println("go rout 结束")
for i := 0; i < 10; i++ {
fmt.Println("写入数据:", i)
c <- i
}
close(c)
}()

time.Sleep(5 * time.Second)

// for i := 0; i < 10; i++ {
// num := <-c

// fmt.Println("读取数据:", num)
// }
for data := range c {
fmt.Println("读取数据:", data)
}
}

  • select: 单流程下一个go只能监控一个chan的状态 ,select 可以完成监控多个channel的状态。多路监控chan状态

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    package main

    import "fmt"

    func fiba(c, quit chan int) {
    a, b := 1, 1
    for {
    select {
    case c <- a:
    a, b = b, a+b
    case <-quit:
    return
    }
    }
    }

    func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
    for i := 0; i < 6; i++ {
    fmt.Println(<-c)
    }
    quit <- 0
    }()
    fiba(c, quit)
    }

    Do not communicate by sharing memory; instead, share memory by communicating

    不要通过共享内存来通信,而要通过通信来共享内存

    它牺牲了部分性能和灵活性,换来了代码清晰性、安全性和可维护性

1
2
3
4
5
6
7
传统并发模型(如 C/C++/Java):
多个线程共享同一块内存(比如全局变量、堆对象);
为了防止数据竞争(race condition),必须用 锁(mutex)、信号量、原子操作等机制来同步;
容易出错:死锁、竞态、性能瓶颈

golang
你不是“一起用”一块内存,而是“把内存里的值传给我”,之后各自独立使用
对比项 传统共享内存 Go 的 channel
通信方式 读写同一变量 发送/接收消息
同步机制 显式加锁(mutex) channel 自带同步语义
数据安全 容易出错(竞态) 编译器+运行时保障
思维模式 “谁在改这个变量?” “数据从哪来,到哪去?”

go modules

1
2
3
4
5
6
7
新项目初始化	go mod init <name>
添加依赖 go get pkg@version
清理/同步依赖 go mod tidy
提交前验证 go mod verify + git add go.mod go.sum
分析依赖来源 go mod why pkg 或 go mod graph
离线构建准备 go mod vendor
go download (单纯下载) 和 go tidy 会修改go.mod go.sum
go.mod 依赖清单:声明项目需要哪些模块及版本要求 Go 版本 只列直接依赖
go.sum 完整性校验:记录每个依赖内容的哈希值,防止篡改 所有传递依赖(整个依赖树)

练习

  1. 照着写 IM系统

Q&A

1. 什么是“逃逸”?

  • 如果一个变量的地址被传递到函数外部(比如作为返回值、被全局变量引用、被 goroutine 捕获等),就说它“逃逸”了。
  • 逃逸的变量必须分配在堆(heap) 上,因为栈内存在函数返回后就销毁了。
  • 未逃逸的变量可以安全地分配在栈(stack) 上,效率更高(分配/回收快,无需 GC)。

​ 不必要的堆分配可能导致的问题,增加 GC 压力(最常见),频繁 GC ,GC 停顿可能影响响应时间


Golang基础
https://blog.dayday.cyou/2025/10/31/golang基础/
作者
godbutton
发布于
2025年10月31日
许可协议