golang 基础案例_01 - 教程

1.make 和 new 的区别

new和make都是在堆上分配内存‌。
        ‌堆(Heap)‌:用于动态内存分配,如使用new或make函数分配的内存。堆中的内存手动分配和释放,因此存在内存泄漏的风险。
 ‌栈(Stack)‌:用于存储局部变量、函数调用的上下文等。栈中的内存由编译器自动管理,不需要手动干预。
new和make的使用场景和区别
new‌:用于为结构体、数组、指针等类型分配内存。它返回一个指向新分配内存的指针。例如,new(int)会分配一个整数大小的内存,并返回一个指向该内存的指针。
make‌:专门用于创建切片(slice)、映射(map)、通道(channel)等内置数据结构。它返回一个对数据结构的引用,而不是指针。例如,make([]int, 0, 100)创建一个长度为0、容量为100的整数切片。

func MakeAndNew() {
// 使用 new 创建一个整数指针
numPtr := new(int)
fmt.Println("numPtr:", *numPtr) // 输出:numPtr: 0,因为 int 类型的零值是 0
// 使用 new 创建一个长度为 5 的整数数组指针
arrPtr := new([5]int)
fmt.Println("arrPtr:", arrPtr) // 输出:arrPtr: &[0 0 0 0 0]
// 使用 make 创建一个切片
slice := make([]int, 3, 5)
fmt.Println("slice:", slice) // 输出:slice: [0 0 0],因为 int 类型的零值是 0
// 使用 make 创建一个map
m := make(map[string]int)
fmt.Println("m:", m) // 输出:m: map[]
// 使用 make 创建一个信道
ch := make(chan int)
fmt.Println("ch:", ch) // 输出:ch: 0xc000016090,信道的地址
}

2.数组与切片

长度固定 vs 动态长度:数组的长度在声明时就确定了,无法改变;而切片的长度可以动态增长或缩小。
内存分配方式:数组在声明时会分配固定大小的连续内存空间;而切片则是引用一个数组,通过指针指向底层数组,并记录切片的长度和容量。
传递方式:数组在函数传递时会进行值拷贝,即传递的是数组的副本;而切片在函数传递时是通过引用传递,传递的是指向底层数组的指针。
长度信息:数组的长度是固定的,通过len()函数获取;而切片有两个长度信息:长度(len())和容量(cap()),分别表示当前切片的实际长度和底层数组的容量。
灵活性:切片可以动态增长或缩小,方便进行数据操作和处理;而数组的长度固定,无法动态改变。
总的来说,数组适合存储固定长度的数据,而切片适合存储不固定长度的数据,并且在实际开发中更常用。

type slice struct {
array unsafe.Pointer // 元素指针
len   int            // 长度
cap   int            // 容量
}
func ArrayAndSlice() {
// 声明一个整数数组
arr := [3]int{}
fmt.Println("arr:", arr)
var a1 [3]int
fmt.Println("a1:", a1)
a2 := [...]int{}
fmt.Println("a2:", a2)
// 声明一个整数切片
slice := []int{1, 2, 3}
fmt.Println("slice:", slice)
// 修改数组的元素
arr[0] = 100
fmt.Println("修改后的 arr:", arr) // 输出:修改后的 arr: [100 0 0]
// 修改切片的元素
slice[0] = 100
fmt.Println("修改后的 slice:", slice) // 输出:修改后的 slice: [100 2 3]
}

3.defer

多个 defer 的顺序,defer 在什么时机会修改返回值?
        defer延迟函数,释放资源,收尾工作;如释放锁,关闭文件,关闭链接;捕获panic;
        defer函数紧跟在资源打开后面,否则defer可能得不到执行,导致内存泄露;
        多个 defer 调用顺序是 LIFO(后入先出),defer后的操作可以理解为压入栈中;
        defer,return,return value(函数返回值) 执行顺序:首先return,其次return value,最后defer。defer可以修改函数最终返回值,修改时机:有名返回值或者函数返回指针。

func Defer1() {
//defer 先进后出方式执行
var whatever [5]struct{}
defer fmt.Println(222)
for i := range whatever {
defer fmt.Println(i)
}
/** 输出:
4
3
2
1
0
222
*/
}
func Defer2() {
unres := unnamedReturn()
fmt.Println("unres:", unres) // 输出:res: 10
nres := namedReturn()
fmt.Println("nres:", nres) // 输出:res: 15
}
func unnamedReturn() int {
result := 10
ret := &result
defer func() {
*ret += 5
fmt.Printf("ret:%v \n", *ret) //15
}()
return result
}
func namedReturn() (result int) {
defer func() {
result += 5
}()
result = 10
return
}

4.uint

uint8-uint64 32位操作系统:uint32 64位操作系统:uint64

func Uint() {
var a uint8 // 0~255(0~2^8-1)
a = 255
var b uint8 = 1
b = 1
fmt.Printf("a=%v,b=%v,a+b=%v \n", a, b, a+b)
}

5.rune

相当int32
        golang中的字符串底层实现是通过byte数组的,中文字符在unicode下占2个字节,在utf-8编码下占3个字节,而golang默认编码正好是utf-8;
        byte 等同于int8,常用来处理ascii字符
        rune 等同于int32,常用来处理unicode或utf-8字符
        rune is an alias for int32 and is equivalent to int32 in all ways. It is used, by convention, to distinguish character values from integer values.

func Rune() {
// 使用 rune 类型表示单个字符
r := 'A'
fmt.Printf("r: %c, type: %T \n", r, r) // 输出:r: A, type: int32
// 使用 rune 类型处理字符串中的字符
s := "Hello, 世界"
runes := []rune(s)
fmt.Println("runes: \n", runes) // 输出:runes: [72 101 108 108 111 44 32 19498 30028]
// 遍历字符串中的字符
for _, r := range s {
fmt.Printf("%c ", r)
}
// 输出:H e l l o ,   世 界
}

6.goroutine 协程

goruntime.GOMAXPROCS(1) 设置最大并发数为1

waitGroup.Wait() 等待所有协程执行完毕

var wg sync.WaitGroup
func Goroutine() {
wg.Add(5)
collectStart := time.Now()
collectStartMills := collectStart.UnixNano() / 1e6
ctx, cancel := context.WithCancel(context.Background())
ctx = context.WithValue(ctx, "StartTime", collectStartMills)
for i := 0; i < 5; i++ {
go func() {
sayHello(ctx, i)
}()
}
go func() {
time.Sleep(time.Second * 2)
cancel()
fmt.Printf("Context cancel\n")
}()
wg.Wait()
fmt.Println("is finished...\n", time.Since(collectStart))
}
func sayHello(ctx context.Context, i int) {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()
go func() {
select {
case <-ctx.Done():
fmt.Println("ctx cancel:", ctx.Err())
wg.Done()
return
}
}()
if i == 4 {
time.Sleep(time.Second * 10)
}
startime := ctx.Value("StartTime").(int64)
fmt.Printf("hello, world %d,time:%v \n", i, startime)
return
}

7.reflect

        TypeOf(nil) 返回的是 reflect.Type 类型,可以通过该类型获取到具体的类型信息,
IsNil()报告v持有的值是否为nil。v持有的值的分类必须是通道、函数、接口、映射、指针、切片之一;否则IsNil函数会导致panic。
        IsValid()返回v是否持有一个值。如果v是Value零值会返回假,此时v除了IsValid、String、Kind之外的方法都会导致panic。
        IsNil()常被用于判断指针是否为空;IsValid()常被用于判定返回值是否有效。

func ReflectGetValue() {
a := 100
atp := reflect.TypeOf(a)
avu := reflect.ValueOf(a)
fmt.Println(atp, avu)
fmt.Println("var a *int IsNil:", reflect.ValueOf(a).IsNil())
// nil值
fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid())
// 实例化一个匿名结构体
b := struct{}{}
// 尝试从结构体中查找"abc"字段
fmt.Println("不存在的结构体成员:", reflect.ValueOf(b).FieldByName("abc").IsValid())
// 尝试从结构体中查找"abc"方法
fmt.Println("不存在的结构体方法:", reflect.ValueOf(b).MethodByName("abc").IsValid())
// map
c := map[string]int{}
// 尝试从map中查找一个不存在的键
fmt.Println("map中不存在的键:", reflect.ValueOf(c).MapIndex(reflect.ValueOf("mm")).IsValid())
}

8.reflect.TypeOf和reflect.ValueOf

        在Go语言的反射机制中,任何接口值都由是一个具体类型和具体类型的值两部分组成的。
        在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由reflect.Type和reflect.Value两部分组成,并且reflect包提供了reflect.TypeOf和reflect.ValueOf两个函数来获取任意对象的Value和Type。

func ReflectSetValue() {
a := 100
if reflect.TypeOf(a).Kind() == reflect.Int {
/**ca := reflect.ValueOf(a)
ca.SetInt(1000)
fmt.Println(ca)  //修改的是副本,reflect包会引发panic
*/
reflect.ValueOf(&a).Elem().SetInt(1000)
}
fmt.Println(a)
}
type Student struct {
Name    string `json:"name"`
Age     int    `json:"age"`
Sex     string `json:"sex"`
Address string `json:"address"`
}

9.eflect.TypeOf()

        任意值通过reflect.TypeOf()获得反射对象信息后,如果它的类型是结构体,
可以通过反射值对象(reflect.Type)的NumField()和Field()方法获得结构体成员的详细信息。

/*Field(i int) StructField    根据索引,返回索引对应的结构体字段的信息。
NumField() int  返回结构体成员字段数量。
FieldByName(name string) (StructField, bool)    根据给定字符串返回字符串对应的结构体字段的信息。
FieldByIndex(index []int) StructField   多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息。
FieldByNameFunc(match func(string) bool) (StructField,bool) 根据传入的匹配函数匹配需要的字段。
NumMethod() int 返回该类型的方法集中方法的数目
Method(int) Method  返回该类型方法集中的第i个方法
MethodByName(string)(Method, bool)  根据方法名返回该类型方法集中的方法
type StructField struct {
// Name是字段的名字。PkgPath是非导出字段的包路径,对导出字段该字段为""。
// 参见http://golang.org/ref/spec#Uniqueness_of_identifiers
Name    string
PkgPath string
Type      Type      // 字段的类型
Tag       StructTag // 字段的标签
Offset    uintptr   // 字段在结构体中的字节偏移量
Index     []int     // 用于Type.FieldByIndex时的索引切片
Anonymous bool      // 是否匿名字段
}*/
func ReflectStruct() {
st1 := Student{
Name:    "zhangsan",
Age:     20,
Sex:     "man",
Address: "beijing",
}
st2 := &Student{
Name:    "lisi",
Age:     21,
Sex:     "man",
Address: "wuhan",
}
/*
st1 是一个 Student 类型的值,存储的是结构体的副本。
st2 是一个指向 Student 类型的指针,存储的是结构体的内存地址。
在实际使用中,如果需要修改原始结构体的内容或者关心性能(例如,避免复制大型结构体),通常会选择使用指针。
如果结构体较小且不需要修改原始内容,可以直接使用值类型。
*/
rt1 := reflect.TypeOf(st1)
fmt.Printf("name:%v,kind:%v \n", rt1.Name(), rt1.Kind())
rt2 := reflect.TypeOf(st2)
fmt.Printf("name:%v,kind:%v \n", rt2.Name(), rt2.Kind())
for i := 0; i < rt1.NumField(); i++ {
field := rt1.Field(i)
fmt.Printf("name:%s index:%d type:%v json tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json"))
}
/*for j := 0; j < rt2.NumField(); j++ {
field := rt2.Field(j)
fmt.Printf("name:%s index:%d type:%v json tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json"))
}
报错panic: reflect: NumField of non-struct type *basic.Student
这个错误信息表明在使用反射(reflect)包时,尝试获取一个非结构体类型的字段数量。具体来说,reflect: NumField of non-struct type *basic.Student 表示你试图对一个 *basic.Student 类型的值调用 NumField 方法,但 *basic.Student 不是一个结构体类型。
在 Go 语言中,reflect 包提供了对结构体字段的反射操作。要正确使用 reflect 包,你需要确保你操作的对象确实是一个结构体类型
*/
}

10.反射的不足

        反射是一个强大并富有表现力的工具,能让我们写出更灵活的代码。但是反射不应该被滥用,原因有以下三个。
        基于反射的代码是极其脆弱的,反射中的类型错误会在真正运行的时候才会引发panic,那很可能是在代码写完的很长时间之后。
        大量使用反射的代码通常难以理解。
        反射的性能低下,基于反射实现的代码通常比正常代码运行速度慢一到两个数量级。

func ReflectGetMethod() {
GetMethod(Student{})
/*
reflect.DeepEqual
reflect.DeepEqual 是 Go 语言中 reflect 包提供的一个函数,用于比较两个值是否深度相等。它会递归地比较两个值的所有字段和元素,包括结构体、数组、切片、映射等复杂类型。
如果两个值的类型和内容都相同,则返回 true,否则返回 false。
需要注意的是:reflect.DeepEqual 在比较某些类型的值时可能会产生意外的结果。例如,它将 nil 切片和空切片视为不相等,将 nil 映射和空映射视为不相等。
此外,它还会比较函数指针的值,这可能导致不正确的结果。因此,在使用 reflect.DeepEqual 时,请确保了解其行为,并在必要时进行适当的自定义比较。
如果你只需要比较基本类型(如整数、浮点数、字符串等)的值,可以使用 == 运算符,而无需使用 reflect.DeepEqual。对于复杂类型,可以考虑使用第三方库,
如 github.com/google/go-cmp,它提供了更强大且灵活的比较功能。
*/
p1 := Person{Name: "Alice", Age: 30}
p2 := Person{Name: "Alice", Age: 30}
p3 := Person{Name: "Bob", Age: 25}
fmt.Println(reflect.DeepEqual(p1, p2)) // 输出:true
fmt.Println(reflect.DeepEqual(p1, p3)) // 输出:false
}
type Person struct {
Name string
Age  int
}
func GetMethod(i interface{}) {
t := reflect.TypeOf(i)
v := reflect.ValueOf(i)
for j := 0; j < v.NumMethod(); j++ {
methodName := t.Method(j).Name
methodType := v.Method(j).Type()
fmt.Printf("method name:%s,method type:%v \n", methodName, methodType)
// 通过反射调用方法传递的参数必须是 []reflect.Value 类型
var args = []reflect.Value{}
v.Method(j).Call(args)
}
}
// 给student添加两个方法 Study和Sleep(注意首字母大写)
func (s Student) Study() string {
msg := "好好学习,天天向上。"
fmt.Println(msg)
return msg
}
func (s Student) Sleep() string {
msg := "好好睡觉,快快长大。"
fmt.Println(msg)
return msg
}

11.select

        golang 的 select 就是监听 IO 操作,当 IO 操作发生时,触发相应的动作。
        在执行select语句的时候,运行时系统会自上而下地判断每个case中的发送或接收操作是否可以被立即执行【立即执行:意思是当前Goroutine不会因此操作而被阻塞,还需要依据通道的具体特性(缓存或非缓存)】。
        每个case语句里必须是一个IO操作;
        所有channel表达式都会被求值、所有被发送的表达式都会被求值;
        如果任意某个case可以进行,它就执行(其他被忽略);
        如果有多个case都可以运行,Select会随机公平地选出一个执行(其他不会执行);
        如果有default子句,case不满足条件时执行该语句;
        如果没有default字句,select将阻塞,直到某个case可以运行;Go不会重新对channel或值进行求值。
        可处理一个或多个 channel 的发送/接收操作。
        如果多个 case 同时满足,select 会随机选择一个执行。
        对于没有 case 的 select 会一直阻塞,可用于阻塞 main 函数,防止退出。

func ChannelSelect() {
chan1 := make(chan int, 4)
chan1 <- 11
select {
case a := <-chan1:
fmt.Printf("a:%v \n", a)
default:
fmt.Println("222")
}
//有缓冲区与无缓冲区
ch2 := make(chan int) //无缓冲通道,需要现有一个goroutine来接收数据,不然向里边发数据会panic
var aaa int
go func() {
aaa = <-ch2
}()
ch2 <- 1
fmt.Println(aaa)
ch3 := make(chan int, 2) //有缓冲通道
ch3 <- 1
ab := <-ch3
fmt.Println(ab)
}
func Channel() {
var wg sync.WaitGroup
ch := make(chan int)
wg.Add(10) // 要与循环里边的wg.Add(1)区别
for i := 0; i < 10; i++ {
go func() {
time.Sleep(time.Second * 2)
ch <- i
//wg.Add(1)
fmt.Printf("ch <- %v \n", i)
}()
}
go func() {
for i := 0; i < 10; i++ {
v := <-ch
wg.Done()
fmt.Printf("%v <- ch  \n", v)
}
}()
close(ch)
wg.Wait()
fmt.Println("main end")
}

12.断言

        在运行时检查接口类型的机制,通常用于确定接口的具体类型,并将其转换为该类型以便进行操作;
有两种方式:t := 变量.(T)  如果断言失败,会直接panic; t,ok := 变量.(T)

func CaseType() {
stu := &Student{
Name: "zhangsan",
Age:  18,
}
detectType(stu)
}
func detectType(req interface{}) {
student, ok := req.(*Student) //断言
if ok {
fmt.Printf("student.Name:%v \n", student.Name)
}
}

posted @ 2025-08-15 13:54  yfceshi  阅读(9)  评论(0)    收藏  举报