Golang学习笔记(三):标准库 && 指针
time包
时间类型 time.Time
time.Time类型表示时间。我们可以通过time.Now()函数获取当前的时间对象,然后获取时间对象的年月日时分秒等信息
func main() {
now := time.Now()
fmt.Printf("time对象: %#v\n", now)
// time.Date(2025, time.October, 23, 22, 5, 33, 186545800, time.Local)
}Time类型封装了一系列方法用于获取给定时间的属性,从%#v的结果也可以看出,分别为:
year := now.Year()
month := now.Month()
day := now.Day()
hour := now.Hour()
minute := now.Minute()
second := now.Second()
nano := now.Nanosecond()
fmt.Printf("现在是%v年%v月%v日 %v时%v分%v秒, 当前时间戳时间戳为%v", year, month, day, hour, minute, second, nano)
// 现在是2025年October月23日 22时8分26秒, 当前时间戳时间戳为840074100Nanosecond函数返回time对象对应的时间戳,如果没有指定,则应该是当前时间;其他函数也应该是同样的道理
诡异time.Time.Format方法
time.Time对象有一个格式化方法Format,可格式化字符串得到time.Time类型。接受下面的字符串格式:
2006-01-02 15:04:05
2006/01/02 15:04:05(这里初见很邪门,必须要用Golang诞生的时间作为模板,而不是更符合经验的Y-M-D模板)
以及12小时制模板:
2006/01/02 03:04:05 PM更全面的模板如下:
func formatDemo() {
now := time.Now()
// 格式化的模板为Go的出生时间2006年1月2号15点04分 Mon Jan
// 24小时制
fmt.Println(now.Format("2006-01-02 15:04:05.000 Mon Jan"))
// 12小时制
fmt.Println(now.Format("2006-01-02 03:04:05.000 PM Mon Jan"))
fmt.Println(now.Format("2006/01/02 15:04"))
fmt.Println(now.Format("15:04 2006/01/02"))
fmt.Println(now.Format("2006/01/02"))
}不要尝试用Format方法去格式化其他字符串,因为这个Format只起到演示作用,告诉你在Golang里可以怎么样写时间字符串
时间戳
获取时间戳
Unix方法返回int64类型的自1970年以来的时间戳
func main() {
timeObj := time.Now()
unixtime := timeObj.Unix()
fmt.Println(unixtime) // 1761231035
}当然,还有一个纳秒时间戳UnixNano(),使用方法就不演示了
时间戳转换成日期字符串
time.Unix的文档:
Unix returns the local Time corresponding to the given Unix time, sec seconds and nsec nanoseconds since January 1, 1970 UTC. It is valid to pass nsec outside the range [0, 999999999]. Not all sec values have a corresponding time value. One such value is 1<<63-1 (the largest int64 value).
Example
Code:
unixTime := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
fmt.Println(unixTime.Unix())
t := time.Unix(unixTime.Unix(), 0).UTC()
fmt.Println(t)
Output:
1257894000
2009-11-10 23:00:00 +0000 UTCfunc main() {
time1 := time.Unix(1761231035, 0).UTC()
// 2025-10-23 14:50:35 +0000 UTC
fmt.Println(time1)
}这次需要更通用的time.Unix,而非time.Time.Unix
获得Time对象后,调用对象的UTC()方法获取UTC日期字符串
日期字符串转换成Time对象
func main() {
// 加载东八区时间
loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
panic(err)
}
// 解析日期字符串
timeString := "2021-01-01 00:00:00"
t, err := time.ParseInLocation("2006-01-02 15:04:05", timeString, loc)
if err != nil {
panic(err)
}
fmt.Printf("%v的格式化时间对象为%v\n", timeString, t)
// 2021-01-01 00:00:00的格式化时间对象为2021-01-01 00:00:00 +0800 CST
}不设置时区就默认是格林威治时区

时间间隔 time.Duration
time.Duration是time包定义的一个类型,它代表两个时间点之间经过的时间,以纳秒为单位。time.Duration表示一段时间间隔,可表示的最长时间段大约290年。
time包中定义的时间间隔类型的常量如下:
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)时间运算
func main() {
now := time.Now()
// 计算一个小时之后的时间
oneHourAfter := now.Add(time.Hour)
// 计算一分钟之前的时间
oneMinuteBefore := now.Add(-time.Minute)
fmt.Printf("当前时间%v一小时之后的时间为%v, 一分钟之前的时间为%v\n", now, oneHourAfter, oneMinuteBefore)
// 当前时间2025-10-23 23:19:57.1119661 +0800 CST m=+0.000000001一小时之后的时间为2025-10-24 00:19:57.1119661 +0800 CST m=+3600.000000001, 一分钟之前的时间为2025-10-23 23:18:57.1119661 +0800 CST m=-59.999999999
// 更新当前时间
beforeTime := now
now = time.Now()
// 比较刚刚和现在的时间是否相等
if beforeTime.Equal(now) {
fmt.Printf("刚刚和现在时间相等\n")
} else {
fmt.Printf("刚刚和现在时间不相等\n")
}
// 刚刚和现在时间不相等
// 判断时间前后
if beforeTime.Before(now) {
fmt.Printf("刚刚时间在现在时间之前\n")
} else {
fmt.Printf("刚刚时间在现在时间之后\n")
}
// 刚刚时间在现在时间之前
}Add
我们在日常的编码过程中可能会遇到要求时间+时间间隔的需求,Go语言的时间对象有提供Add方法如下:
func (t Time) Add(d Duration) Time举个例子,求一个小时之后的时间:
func main() {
now := time.Now()
later := now.Add(time.Hour) // 当前时间加1小时后的时间
fmt.Println(later)
}Sub
求两个时间之间的差值:
func (t Time) Sub(u Time) Duration返回一个时间段t-u。如果结果超出了Duration可以表示的最大值/最小值,将返回最大值/最小值。要获取时间点t-d(d为Duration),可以使用t.Add(-d)。
Equal
func (t Time) Equal(u Time) bool判断两个时间是否相同,会考虑时区的影响,因此不同时区标准的时间也可以正确比较。本方法和用t==u不同,这种方法还会比较地点和时区信息。
Before
func (t Time) Before(u Time) bool如果t代表的时间点在u之前,返回真;否则返回假。
After
func (t Time) After(u Time) bool如果t代表的时间点在u之后,返回真;否则返回假。
定时器 time.Tick
使用time.Tick(时间间隔)来设置定时器,定时器的本质上是一个通道(channel)。
func tickDemo() {
ticker := time.Tick(time.Second) //定义一个1秒间隔的定时器
for i := range ticker {
fmt.Println(i)//每秒都会执行的任务
}
}指针
指针是指向另一个变量的内存地址变量
func main() {
a := 127
fmt.Printf("[%T]%#v at %#v\n", a, a, &a)
// [int]127 at (*int)(0xc00000a0e8)
fmt.Printf("[%T]%#v at %#v\n", &a, a, &a)
// [*int]127 at (*int)(0xc00000a0e8)
}取地址和解引用
使用&取地址符取出变量的地址:
ptf := &v- 如果
v的类型是T,那么ptr的类型就是* T
相对的,还有解引用符:
v := *ptr指针是引用类型
接受指针传参的函数可能会污染指针所指向的变量
func main() {
a := []int{127, 63, 31}
fmt.Printf("[%T]%#v at %#v\n", &a, a, &a)
// [*[]int][]int{127, 63, 31} at &[]int{127, 63, 31}
func(x *int) {
*x = 31
}(&(a[1]))
fmt.Printf("[%T]%#v at %#v\n", &a, a, &a)
// [*[]int][]int{127, 31, 31} at &[]int{127, 31, 31}
// 类型是*[]int, 而#v会自动解引用指针
}传递引用类型时传递的只是内存地址,函数不会自动创建传参的副本;有这个需求的建议自己手动创建副本
上面的%#v自动解引用了我对切片的指针;需要使用%p才能打印出切片指针:
func main() {
a := []int{127, 63, 31}
fmt.Printf("[%T]%#v at %p\n", &a, a, &a)
// [*[]int][]int{127, 63, 31} at 0xc000008030
func(x *int) {
*x = 31
}(&(a[1]))
fmt.Printf("[%T]%#v at %p\n", &a, a, &a)
// [*[]int][]int{127, 31, 31} at 0xc000008030
}返回指针的函数
在C语言中,退出函数后函数堆栈会立刻被销毁,所以不允许从函数中返回在函数中分配了内存的堆变量或指针,因为函数销毁后指针指向的内存空间也会被释放,得到野指针
但Golang有现代GC :
相关信息
原理:
三色抽象:GC 把内存中的所有对象分为三类:
白色 (White):潜在的垃圾。GC 开始时,所有对象都是白色的。
灰色 (Gray):已被标记,但它的子对象(它引用的其他对象)还没被扫描。
黑色 (Black):已被标记,且它的所有子对象都已被扫描。黑色对象是存活的。
工作流程:
GC 从根对象开始,将它们放入灰色集合。
GC 从灰色集合中取出一个对象,把它涂成黑色。
然后扫描这个对象引用的所有白色子对象,把它们涂成灰色,并放入灰色集合。
重复第 2、3 步,直到灰色集合为空。
此时,所有仍然是白色的对象,就是不可达的垃圾,可以被回收。
【核心优势】:并发执行
这是 Go GC 的“杀手锏”。Go 的 GC 绝大部分的标记(扫描)工作,是与你的应用程序代码(Goroutines)并发运行的,而不是长时间地暂停整个程序。
它通过“写屏障 (Write Barrier)”技术来保证并发标记的正确性。简单来说,当你的程序在 GC 运行时修改对象指针时(比如 a.child = b),会触发一段额外的代码,通知 GC 这个变化,防止 GC 错误地回收掉存活的对象。
极短的暂停时间 (STW - Stop-the-World)
Go 的 GC 仍然需要在某些阶段(比如开启标记、结束标记)进行极短的、通常在亚毫秒级别的全局暂停 (STW)。
Go 团队的主要目标之一,就是不断地缩短这个 STW 的时间。在现代 Go 版本中,这个暂停时间已经短到对于绝大多数网络服务来说几乎可以忽略不计。
缺点:
非实时:垃圾不是立即被回收的。它会累积到一定阈值(比如内存增长了一倍)后,才触发一次 GC 周期。
更复杂:算法本身比引用计数复杂得多。
Go 的函数可以返回一个指针,意味着这个函数创建了一个数据,然后把指向这个数据的“内存地址门牌号”交给了调用者。
这个行为之所以在 Go 中是完全安全的,是因为 Go 语言有一个聪明的机制叫做 “逃逸分析 (Escape Analysis)”
package main
import "fmt"
type User struct {
Name string
}
// 这个函数返回一个指向 User 的指针
func NewUser(name string) *User {
// u 在这里被创建
u := User{Name: name}
// Go 编译器在这里进行“逃逸分析”
// 它发现 u 的地址 (&u) 被作为返回值返回了,
// 意味着它的生命周期必须比 NewUser 函数更长。
// u 将会“逃逸”出当前函数。
return &u
}
func main() {
// userPtr 接收到了那个安全的指针
userPtr := NewUser("Alice")
// 我们可以安全地解引用并使用它
fmt.Println(userPtr.Name) // 输出: Alice
}- 在编译 NewUser 函数时,编译器会进行逃逸分析。
- 它发现变量 u 的地址 &u 被 return 了,即将被函数外部的代码所引用。
- 编译器判断,如果把 u 分配在栈 (Stack) 上,那么当 NewUser 函数返回时,u 的内存就会失效,这会产生一个悬垂指针。这是不安全的。
- 因此,编译器会自动做出一个决定:不把 u 分配在栈上,而是把它分配在【堆 (Heap)】上。
- 堆内存由 Go 的垃圾回收器 (GC) 来管理,它的生命周期与函数调用无关。只要还有任何指针指向这块内存,它就会一直存活。
- NewUser 函数返回的,就是一个指向这块安全的、位于堆上的内存的指针。
Go 的编译器通过逃逸分析,自动地决定了一个变量应该分配在栈上(如果它只在函数内部使用)还是堆上(如果它需要在函数外部继续存活)。
返回指针的意义是什么?
- 避免大数据拷贝:
如果 User 是一个非常大的结构体,返回一个 User 类型的值会拷贝整个结构体。而返回一个 *User(指针)只拷贝一个 8 字节的内存地址,效率极高。 - 共享可变状态:
当你把一个指针传递给多个函数时,这些函数都可以通过这个指针来修改同一个 User 实例。这在需要共享和修改状态时非常有用(但也要注意并发安全)。 - 表示“可选”或“可空”的值:
一个指针的零值是 nil。这让你可以用 nil 来表示“没有这个对象”的状态,而一个结构体值本身做不到这一点(它的零值是一个所有字段都是零值的实例)。
为指针分配内存
new函数创建指针变量(并分配内存)
func new(Type) *Typenew用于类型的内存分配,且内存对应的值会初始化为零值 :
func main() {
a := new(bool)
fmt.Println(*a) // false
b := new([]int)
fmt.Println(*b) // []
c := new(map[int]string)
fmt.Println(*c) // map[]
}make函数分配内存空间
不同于new,make只用于切片、键值对和channel的内存创建,返回值类型就是参数类型本身
结构体
【补课】 type关键字
定义类型:
type NewType OldTypeNewType是新的类型
类型别名(Go 1.9+):
type AlisedType = TypeAlisedType只是等号右边的类型的别名
rune和byte就是一种类型别名:
type byte = uint8
type rune = int32对于函数类型和复合数据类型,建议活用type关键字,保持代码可读性和可维护性
看这一章的函数函数一节,就用到了type关键字来重命名函数类型
结构体(一):声明、示例和访问结构体
type structType struct {
attr1 attrType1
attr2 attrType2
...
}类型推导实例化结构体
type person struct {
name string
age int
sex rune
}
func main() {
// var p person // 可以先创建变量再初始化
p := person{name: "Joe", age: 20, sex: 'M'} // 依旧类型推导
fmt.Printf("仅打印结构体的字段值: %v\n", p)
// 仅打印结构体的字段值: {Joe 20 77}
fmt.Printf("打印结构体的字段名和对应的字段值: %+v\n", p)
// 打印结构体的字段名和对应的字段值: {name:Joe age:20 sex:77}
fmt.Printf("打印Go语法的结构体字段(包括字段名和对应的值): %#v\n", p)
// 打印Go语法的结构体字段(包括字段名和对应的值): main.person{name:"Joe", age:20, sex:77}
}顺便演示一下%+v和%#v如何用于结构体调试
直接赋值时可以不写键:
访问和修改结构体的字段:
func main() {
p := person{name: "Joe", age: 20, sex: 'M'}
fmt.Printf("Name: %s, Age: %d, Sex: %c\n", p.name, p.age, p.sex)
// Name: Joe, Age: 20, Sex: M
p.name = "Alice"
fmt.Printf("Name: %s, Age: %d, Sex: %c\n", p.name, p.age, p.sex)
// Name: Alice, Age: 20, Sex: M
}直接p.attribute访问或修改值
new函数实例化结构体
用new也可以
func main() {
pointerP := new(person)
*pointerP = person{"Joe", 20, 'M'}
fmt.Printf("Name: %s, Age: %d, Sex: %c\n", pointerP.name, pointerP.age, pointerP.sex)
// Name: Joe, Age: 20, Sex: M
pointerP.name = "Alice"
fmt.Printf("Name: %s, Age: %d, Sex: %c\n", pointerP.name, pointerP.age, pointerP.sex)
// Name: Alice, Age: 20, Sex: M
}并且结构体指针访问字段的语法也是:
ptr.attribute底层实际上是:
(*ptr).attribute直接对结构体字面量取地址来实例化结构体
func main() {
pointerP := &person{}
*pointerP = person{"Joe", 20, 'M'}
fmt.Printf("Name: %s, Age: %d, Sex: %c\n", pointerP.name, pointerP.age, pointerP.sex)
// Name: Joe, Age: 20, Sex: M
pointerP.name = "Alice"
fmt.Printf("Name: %s, Age: %d, Sex: %c\n", pointerP.name, pointerP.age, pointerP.sex)
// Name: Alice, Age: 20, Sex: M
}结构体(二):结构体方法
func (receiverName receiverType) funcName(
// 参数列表
) (
// 返回值列表
)receiverName:接收者变量名;最好别写self或this,而使用结构体名称的缩写或首字母,比如user就是u,person就是preceiverType:接收者类型;可以是指针类型,也可以不是
后面的签名部分和一般函数一样
示例:
func (p *person) nameSetter(newName string) {
p.name = newName
}
func (p person) ageGetter() int {
return p.age
}
func main() {
p := person{name: "James", age: 20, sex: 'M'}
fmt.Printf("%+v\n", p)
// {name:James age:20 sex:77}
p.nameSetter("JamesBond") // 方法是自动依附在结构体上的
fmt.Printf("%+v\n", p)
// {name:JamesBond age:20 sex:77}
}- Go不建议这样写
setter和getter;这里只是为了演示 - 有修改原值的操作,就要写指针类型的接收者类型,没有的话指针不指针都可以
- 不管是不是指针类型的接收者类型,最终都会依附到指针解引用后的接收者类型上
这里的nameSetter的接收者类型就是指针,但编译器仍可以将方法吸附到person结构体上
- 不管是不是指针类型的接收者类型,最终都会依附到指针解引用后的接收者类型上
初识receiver方法:给任意类型添加方法
receiver机制也不是专为结构体设计的,任何类型都可以写方法;比如给int类型写新方法:
注意:当目标类型是非本地的类型时,要用类型定义声明一个新类型,Golang编译器不允许开发者对非本地的包中的类型添加方法
type myInt int
func (num myInt) print() {
fmt.Println(num)
}
func main() {
var a myInt = 31
a.print()
// 31
}类型别名则不会创建新类型,编译器知道OldType原本在哪里(在builtin包里,要改只能去builtin包改):
匿名字段
type person struct {
string
int
rune
}使用匿名字段时,类型名就是字段名,而结构体声明要求同名字段只能有一个,所以使用匿名字段时一个类型只能有一个字段
嵌套结构体
结构体字段可以为引用类型:
type person struct {
name string
age int
phones map[string]string // 国家属地和对应的电话号码前缀
}
func main() {
p := person{
name: "张三",
age: 18,
phones: map[string]string{
"中国": "+86",
"加拿大": "+1",
},
}
fmt.Printf("%#v\n", p)
// main.person{name:"张三", age:18, phones:map[string]string{"中国":"+86", "加拿大":"+1"}}
}也可以为另一个结构体类型:
type person struct {
name string
age int
phones []phone
}
type phone struct {
country string
prefix string
}
func main() {
p := person{
name: "张三",
age: 18,
phones: []phone{
phone{
country: "中国",
prefix: "+86",
},
phone{
country: "加拿大",
prefix: "+1",
},
},
}
fmt.Printf("%#v\n", p)
// main.person{name:"张三", age:18, phones:[]main.phone{main.phone{country:"中国", prefix:"+86"}, main.phone{country:"加拿大", prefix:"+1"}}}
}结构体顺序不分先后
对于嵌套结构体和外部结构体都有的字段,Golang会先在外部结构体里搜索,搜不到才会去嵌套结构体里接着找:
type person struct {
name string
age int
phone
country string
}
type phone struct {
country string
prefix string
}
func main() {
p := person{
name: "张三",
age: 18,
country: "CN",
phone: phone{
country: "中国",
prefix: "+86",
},
}
fmt.Printf("%#v\n", p)
// main.person{name:"张三", age:18, phone:main.phone{country:"中国", prefix:"+86"}, country:"CN"}
fmt.Println(p.country, p.prefix)
//CN +86
}具名嵌套结构体就没有这样的机制
结构体继承
使用嵌套匿名结构体实现
type animal struct {
name string
}
type cat struct {
animal
}
func (c cat) meow() {
fmt.Println("meow")
}
func main() {
c := cat{}
c.meow()
}
结构体(三):序列化与反序列化
结构体与JSON互转
使用json.Marshal方法序列化实例;使用json.Unmarshal反序列化[]bytes类型的JSON字符串
type Person struct {
Name string
Age int
Country string
}
func main() {
p1 := Person{Name: "James", Age: 20, Country: "Canada"}
fmt.Printf("%+v\n", p1)
jsonByte, _ := json.Marshal(p1)
jsonStr := string(jsonByte)
fmt.Printf("%v\n", jsonStr)
// {"Name":"James","Age":20,"Country":"Canada"}
p2 := Person{}
err := json.Unmarshal(jsonByte, &p2) // json字节序列和对应类型的指针
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("[%T] %#v\n", p2, p2)
// [main.Person] main.Person{Name:"James", Age:20, Country:"Canada"}
}注意:以小写字母开头的字段为私有字段,json包无法访问
标记键
``反引号包裹的字符串置于结构体字段后面,可以修改JSON等格式导出字符串中的字段名:
type Person struct {
Name string
Age int
Country string `json:"country"` // 可以改成别的标签名
}
func main() {
p1 := Person{Name: "James", Age: 20, Country: "Canada"}
fmt.Printf("%+v\n", p1)
jsonByte, _ := json.Marshal(p1)
jsonStr := string(jsonByte)
fmt.Printf("%v\n", jsonStr)
// {"Name":"James","Age":20,"country":"Canada"}
// {"Name":"James","Age":20,"xxx":"Canada"}
p2 := Person{}
err := json.Unmarshal(jsonByte, &p2) // json字节序列和对应类型的指针
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("[%T] %#v\n", p2, p2)
// 还是[main.Person] main.Person{Name:"James", Age:20, Country:"Canada"}
// 改成xxx也照样可以序列化回来,只要键值对顺序没变
}嵌套结构体的JSON序列化与反序列化
一样一样
type Person struct {
Name string
Age int
Country string `json:"lalala"`
Phone
}
type Phone struct {
Country string
Prefix string
}
func main() {
p := Person{
Name: "Jon",
Age: 20,
Country: "China",
Phone: Phone{
Country: "China",
Prefix: "86",
},
}
fmt.Printf("%+v\n", p) // {Name:Jon Age:20 Country:China Phone:{Country:China Prefix:86}}
jsonBytes, _ := json.Marshal(p)
fmt.Println(string(jsonBytes))
// {"Name":"Jon","Age":20,"lalala":"China","Country":"China","Prefix":"86"}
p2 := Person{}
err := json.Unmarshal(jsonBytes, &p2)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", p2)
// {Name:Jon Age:20 Country:China Phone:{Country:China Prefix:86}}
}