Golang学习
后端学习一般路线
1.后端编程语言(基础语法)
2.后端框架(用于快速构建一套API)
3.软件包管理器
微服务
将每个单独的方法拿出来,放在一个服务器中
副数据库选择
比如快速搜索采用Elasticsearch,缓存使用redis提高性能,异步通信采用Rabbit MQ这样的消息队列
环境搭建
安装链接:下载并安装 - The Go Programming Language
检验安装
1 | go version |
当然可以!以下是你提供的 Golang 笔记标题的补全内容:
语法
Go语言的语法设计简洁明了,主要特点包括静态类型、编译型、垃圾回收、并发支持等。以下是一些关键语法点:
基本语法
变量声明:使用
var
关键字或:=
短声明形式。1
2var x int
y := 10变量声明使用
var
关键字显式声明类型,或者使用:=
自动推断类型。常量:使用
const
关键字。1
const Pi = 3.14
常量声明使用
const
关键字,值在编译时确定,不能修改。数据类型:基本类型包括
int
,float64
,string
,bool
等。1
2
3
4var a int = 1
var b float64 = 2.5
var c string = "Hello"
var d bool = trueGo语言支持多种基本数据类型,使用
var
关键字声明并初始化。控制结构:包括
if-else
、for
循环、switch-case
等。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18if x > 0 {
// do something
} else {
// do something else
}
for i := 0; i < 10; i++ {
fmt.Println(i)
}
switch day {
case "Monday":
fmt.Println("Start of the week")
case "Friday":
fmt.Println("End of the work week")
default:
fmt.Println("Midweek")
}Go语言的控制结构类似于其他C系语言,
if
语句和for
循环都非常直观,switch
语句支持多种情况。
函数
函数声明:
1
2
3func add(a int, b int) int {
return a + b
}函数声明使用
func
关键字,参数类型和返回类型都必须明确。多返回值:
1
2
3func swap(x, y string) (string, string) {
return y, x
}Go函数可以返回多个值,常用于返回结果和错误信息。
匿名函数和闭包:
1
2
3
4sum := func(a, b int) int {
return a + b
}
fmt.Println(sum(3, 4)) // 输出: 7Go支持匿名函数和闭包,可以在函数内部定义并使用函数。
数组
声明和初始化:
1
2
3var arr [5]int
arr[0] = 1
arr := [5]int{1, 2, 3, 4, 5}数组有固定长度,声明时需指定长度,初始化时可以使用字面值。
切片(Slices)
由于数组是固定的,在实际的应用场景中受限很大,所以通过切片(Slices)提供更加灵活的数组
语法上只需要省略定义数组时所需要写的长度即可
需要注意的是切片底层是使用指针的,如果你直接把一个变量=
该切片的话,改变切片的值的同时,该变量也会一起改变
声明和初始化:
1
2var s []int
s = append(s, 1, 2, 3)切片是动态数组,可以动态调整大小,常用
append
函数追加元素。切片操作:
1
2s := []int{1, 2, 3, 4, 5}
sub := s[1:3] // sub == []int{2, 3}切片可以从一个数组或另一个切片中截取部分元素。
映射 (Maps)
声明和初始化:
1
2
3
4var m map[string]int
m = make(map[string]int)
m["one"] = 1
m := map[string]int{"one": 1, "two": 2}映射是键值对集合,使用
make
函数创建并初始化。
循环(range)
遍历数组和切片:
1
2
3for i, v := range arr {
fmt.Println(i, v)
}使用
range
关键字遍历数组和切片。遍历映射:
1
2
3for k, v := range m {
fmt.Println(k, v)
}使用
range
关键字遍历映射,获取键和值。
字符串(string)
声明和操作:
1
2str := "Hello, 世界"
fmt.Println(len(str)) // 字符串长度(字节数)字符串是不可变的字节序列,可以获取长度和进行切片操作。
符文(Runes)
Runes
的存在是因为字符串的底层原因,会导致字符串长度并不是字符数,而是字节数,因为比如说中文表达为uft-8
时,需要两份字节,所以最后len(str)
会多出一格长度,而Runes
则能够正常获取到字符数
声明和操作:
1
2
3
4
5
6
7var mystring = []rune("resume")
var indexed = myString[1]
fmt.Printf("%v, %T\n", indexed, indexed)
for i, v := range myString {
fmt.Println(i, v)
}rune
表示单个Unicode字符
结构体 (Structs)
结构体是一种聚合数据类型,它将多个不同类型的字段组合成一个类型,用于表示一个实体。例如,一个人的信息可以用一个结构体来表示。
声明和初始化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22package main
import "fmt"
// 定义一个结构体类型
type Person struct {
Name string
Age int
}
func main() {
// 创建一个结构体实例
p := Person{Name: "Alice", Age: 30}
// 访问结构体字段
fmt.Println(p.Name) // 输出: Alice
fmt.Println(p.Age) // 输出: 30
// 修改结构体字段
p.Age = 31
fmt.Println(p.Age) // 输出: 31
}结构体是用户定义的类型,包含多个字段,可以使用字面值初始化。
接口 (Interfaces)
接口是一种抽象类型,它定义了一组方法(方法集),但是并不实现它们。任何类型只要实现了接口中的方法,就被认为是实现了该接口。接口在Go语言中用于实现多态和解耦。
声明和实现:
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
44package main
import "fmt"
// 定义一个接口
type Speaker interface {
Speak()
}
// 定义一个结构体类型
type Person struct {
Name string
}
// 为结构体类型实现接口中的方法
func (p Person) Speak() {
fmt.Println(p.Name, "says hello!")
}
// 定义另一个结构体类型
type Dog struct {
Name string
}
// 为结构体类型实现接口中的方法
func (d Dog) Speak() {
fmt.Println(d.Name, "barks!")
}
func main() {
// 创建结构体实例
p := Person{Name: "Alice"}
d := Dog{Name: "Buddy"}
// 定义一个接口类型的变量
var s Speaker
// 将结构体实例赋值给接口类型变量
s = p
s.Speak() // 输出: Alice says hello!
s = d
s.Speak() // 输出: Buddy barks!
}接口定义一组方法,任何实现这些方法的类型都隐式实现了该接口。
指针 (Pointers)
指针实际上是为了减少因为创建数据副本,导致多出来的性能消耗,这点在函数中尤为明显,因为正常的函数传递值都是去创建副本,而如果指定参数为指针的话就可以改变这一点
指针声明和操作:
1
2
3
4var p *int
x := 5
p = &x
fmt.Println(*p) // 输出: 5指针保存变量的内存地址,使用
&
获取地址,使用*
解引用指针。修改指针指向的值:
1
2*p = 10
fmt.Println(x) // 输出: 10通过指针可以修改变量的值。
使用指针的原因和场景
- 修改变量的值:
函数传参时,默认是值传递,这意味着函数内部修改参数的副本,而不影响原变量。如果希望函数能够修改传入的变量,可以使用指针。
例子:
1
2
3
4
5
6
7
8
9
10
11
12
13package main
import "fmt"
func increment(x *int) {
*x = *x + 1
}
func main() {
var num int = 5
increment(&num) // 传递num的指针
fmt.Println("increment 后的 num:", num) // 输出: increment 后的 num: 6
}- 提高性能:
在处理大数据结构(如大型数组或结构体)时,传递指针而不是整个数据结构,可以避免数据的拷贝,减少内存和时间开销。
例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17package main
import "fmt"
type LargeStruct struct {
data [1000]int
}
func modifyStruct(ls *LargeStruct) {
ls.data[0] = 1
}
func main() {
var ls LargeStruct
modifyStruct(&ls)
fmt.Println("修改后的 LargeStruct:", ls.data[0]) // 输出: 修改后的 LargeStruct: 1
}- 共享数据:
当多个函数或多个goroutine需要共享和修改相同的数据时,可以使用指针。
例子:
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
33package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Increment() {
c.mu.Lock()
c.value++
c.mu.Unlock()
}
func main() {
counter := &Counter{}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
fmt.Println("最终计数器的值:", counter.value) // 输出: 最终计数器的值: 1000
}
Go routines (线程)
Goroutines 是 Go 语言(Golang)中的一种轻量级线程。它们允许你在同一地址空间中并发执行函数,具有高效的内存和处理开销。Goroutines 是 Go 并发编程的核心特性,能够简化多线程编程的复杂性。
Go routines 的作用
- 并发执行:Goroutines 使得多个函数可以并发执行,而不是依次执行,从而提升程序的性能。
- 轻量级:相比操作系统线程,Goroutines 更轻量级,启动和切换的开销更小。
- 独立性:每个 Goroutine 都有自己的调用栈,并且该栈大小可以动态增长。
Go routines 的使用
要启动一个 Goroutine,只需在函数调用前加上 go
关键字。例如:
1 | package main |
代码解释
- **函数
say
**:该函数接收一个字符串参数,并在一个循环中打印该字符串。 - 启动 Goroutine:在
main
函数中,使用go say("world")
启动一个新的 Goroutine 并发执行say
函数。 - 主 Goroutine:主 Goroutine 继续执行
say("hello")
,与新的 Goroutine 并发运行。
并发问题及其解决
如果在 main
函数中启动 Goroutines 后立即返回,程序会退出,导致 Goroutines 可能没有机会执行。可以使用 sync.WaitGroup
或 time.Sleep
来解决这个问题。
使用 sync.WaitGroup
sync.WaitGroup
用于等待一组 Goroutines 完成。
1 | package main |
使用 time.Sleep
另一种方法是使用 time.Sleep
暂时阻止 main
函数退出,以便让 Goroutines 有时间执行。
1 | package main |
并发存入数据及其解决
当你希望将 Goroutines 产生的结果存入一个共享的切片时,可能会遇到并发写入问题,因为多个 Goroutines 同时写入同一个切片可能会导致数据竞争和不一致的问题。为了解决这个问题,你可以使用同步机制来保护对切片的并发访问。以下是两种常见的解决方案:使用 sync.Mutex
或使用 Channels。
使用 sync.Mutex
sync.Mutex
提供了一种互斥锁机制,确保在同一时间只有一个 Goroutine 可以访问共享资源。
示例代码
1 | package main |
代码解释
- 引入
sync
包:导入sync
包以使用WaitGroup
和Mutex
。 - **声明
WaitGroup
和Mutex
**:在main
函数中声明WaitGroup
变量wg
和Mutex
变量mu
。 - 定义
worker
函数:该函数接收一个 ID、WaitGroup
、Mutex
和结果切片。计算结果后,使用mu.Lock()
和mu.Unlock()
保护对切片的并发写入。 - 启动 Goroutines:在
main
函数中启动多个 Goroutines,传递wg
、mu
和结果切片的指针。 - 等待 Goroutines 完成:使用
wg.Wait()
等待所有 Goroutines 完成。 - 打印结果:输出结果切片。
使用 Channels
Channels 提供了一种安全的并发数据传递方式,可以避免数据竞争。
示例代码
1 | package main |
代码解释
- 创建通道:在
main
函数中创建一个带缓冲区的通道ch
。 - 定义
worker
函数:该函数接收一个 ID、WaitGroup
和通道。计算结果后,将结果发送到通道。 - 启动 Goroutines:在
main
函数中启动多个 Goroutines,传递wg
和通道。 - 关闭通道:启动一个 Goroutine 来等待所有
worker
完成,然后关闭通道ch
。 - 读取通道数据:使用
range
从通道中读取数据并将其追加到结果切片中。 - 打印结果:输出结果切片。
Channels
Golang(也称为Go)中的Channels是用于在不同goroutine之间进行通信和同步的机制。它们可以让你安全地在多个goroutine之间传递数据,而无需使用复杂的锁机制。Channels可以看作是一个管道,通过它一个goroutine可以将数据发送给另一个goroutine。
声明和创建
你可以使用make
函数来创建一个channel,指定其传递的数据类型:
1 | ch := make(chan int) |
这里我们创建了一个传递int
类型数据的channel。
发送和接收
使用<-
操作符来发送和接收数据:
1 | // 发送数据到channel |
无缓冲和有缓冲的Channels
- 无缓冲Channel:发送操作会阻塞直到另一个goroutine准备好接收这个值,接收操作会阻塞直到另一个goroutine发送一个值。
- 有缓冲Channel:你可以在创建channel时指定缓冲区的大小。发送操作只有在缓冲区满时才会阻塞,接收操作只有在缓冲区为空时才会阻塞。
1 | // 创建一个缓冲区大小为2的channel |
基本例子
下面是一个简单的例子,展示了如何在两个goroutine之间使用channel进行通信:
1 | package main |
在这个例子中,主goroutine启动了一个新的goroutine,该goroutine在2秒后向channel发送一条消息。主goroutine则阻塞在接收操作,直到收到消息并打印出来。
使用有缓冲Channel
下面的例子展示了如何使用有缓冲的channel:
1 | package main |
在这个例子中,channel有一个缓冲区,可以存放两个int
值。我们在发送两个值后,从channel接收并打印它们。
Select语句
select
语句可以让你同时等待多个channel操作。它类似于switch
语句,但每个case都必须是一个channel操作。
1 | package main |
在这个例子中,我们启动了两个goroutine,每个goroutine在不同的时间间隔后向各自的channel发送消息。select
语句用于等待任意一个channel的消息,并打印接收到的消息。
泛型 (Generics)
在Go语言(Golang)中,Generics(泛型)是一种允许编写更加灵活和可重用代码的特性。通过使用泛型,可以定义能够处理多种数据类型的函数和数据结构,而无需为每种类型单独编写代码。Go 1.18开始正式支持泛型。
泛型函数
一个泛型函数可以接收任意类型的参数。例如,一个用于获取切片中最大值的泛型函数:
1 | package main |
在这个例子中,Max
函数使用了类型参数 T
,并且通过 Ordered
接口约束了 T
必须是支持比较运算的类型。
泛型数据结构
你也可以使用泛型来定义数据结构。例如,一个简单的栈(Stack)数据结构:
1 | package main |
在这个例子中,Stack
数据结构使用了泛型类型参数 T
,因此可以创建处理不同类型的栈实例。
网络请求框架
在Golang中,常用的网络请求框架包括 net/http
和 gin
等。
net/http
- 创建简单的HTTP服务器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
gin
- 使用
gin
框架创建API:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // 默认监听并服务于 0.0.0.0:8080
}
连接数据库实现增删改查
Golang 提供了多种方法来连接和操作数据库,常用库有 database/sql
和 gorm
。
使用 database/sql
连接数据库:
1
2
3
4
5
6
7
8
9
10
11
12import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/dbname")
if err != nil {
panic(err)
}
defer db.Close()
}查询数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14rows, err := db.Query("SELECT id, name FROM users")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id int
var name string
if err := rows.Scan(&id, &name); err != nil {
log.Fatal(err)
}
fmt.Println(id, name)
}插入数据:
1
2
3
4
5
6
7
8
9
10
11
12
13stmt, err := db.Prepare("INSERT INTO users(name) VALUES(?)")
if err != nil {
log.Fatal(err)
}
res, err := stmt.Exec("John Doe")
if err != nil {
log.Fatal(err)
}
lastId, err := res.LastInsertId()
if err != nil {
log.Fatal(err)
}
fmt.Println(lastId)
使用 gorm
连接数据库:
1
2
3
4
5
6
7
8
9
10
11
12import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
dsn := "user:password@tcp(127.0.0.1:3306)/dbname"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
}定义模型并迁移:
1
2
3
4
5
6type User struct {
ID uint
Name string
}
db.AutoMigrate(&User{})插入数据:
1
db.Create(&User{Name: "John Doe"})
查询数据:
1
2
3var user User
db.First(&user, 1) // 查询主键为1的用户
db.First(&user, "name = ?", "John Doe")更新数据:
1
db.Model(&user).Update("Name", "Jane Doe")
删除数据:
1
db.Delete(&user, 1)
微服务
Golang 在微服务开发中非常流行,常用框架有 go-micro
和 grpc
。
go-micro
- 创建微服务:
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
27package main
import (
"github.com/micro/go-micro/v2"
"context"
"fmt"
)
type Greeter struct{}
func (g *Greeter) Hello(ctx context.Context, req *Request, rsp *Response) error {
rsp.Msg = "Hello " + req.Name
return nil
}
func main() {
service := micro.NewService(
micro.Name("greeter"),
)
service.Init()
micro.RegisterHandler(service.Server(), new(Greeter))
if err := service.Run(); err != nil {
fmt.Println(err)
}
}
grpc
- 创建 gRPC 服务:
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
31package main
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
"log"
"net"
pb "path/to/proto"
)
type server struct {
pb.UnimplementedGreeterServer
}
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
reflection.Register(s)
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
以上是各个部分的补全内容,希望对你有所帮助!
项目Demo
GORM+Gin实现API
好的,我们将项目修改为使用 GORM 框架来代替原生 SQL 查询,并将 Item
更改为一个更具体的名称,例如 Product
。以下是修改后的代码和项目结构:
1 | gin-mysql-api/ |
1. main.go
主文件,用于启动应用程序。
1 | package main |
2. config/config.go
配置文件,包含数据库连接的配置。
注意这里的?charset=utf8&parseTime=true
不配置这个的话,会导致无法访问时间类的字段,导致报错,无法返回对应的数据
1 | package config |
3. database/database.go
数据库初始化和连接管理。
1 | package database |
4. models/product.go
定义数据模型。
1 | package models |
5. controllers/product.go
处理器函数,实现增删改查操作。
1 | package controllers |
6. routers/router.go
设置路由。
1 | package routers |
完整的 go.mod
文件
确保你在项目根目录下初始化了 go mod
并安装了必要的依赖项。
1 | go mod init gin-mysql-api |
这样,你的项目使用了 GORM 框架来简化数据库操作,并且代码结构更加清晰和易于维护。