Go语言学习

变量

变量定义

// 自动推断
var b = 22

//显式声明
var name string = "haha"
var a int = 1

//简短声明,Go会自动推断类型
a := 1

//同时声明多个变量
var a,b = 1,"hello"
a,b := 1,"hello"
var a,b,c int = 1,2,3

//匿名变量
var _ = 1

var为go中定义变量的固定关键字,当然也可以使用简短声明符:来声明变量。而变量的类型可以不指出。

go中声明的变量必须使用,不然会报错

go中的匿名变量不占用空间,不分配内存。任何赋给它的值都会被抛弃,因此不能在后续的代码中使用。并且匿名变量可以多次声明,甚至可以同时声明。表示符为 _

//go中的函数可以直接返回多个值,匿名变量也常用于占位接收那些暂时用不到的返回值

func text()(int,int,int){
	return 1,2,3
}

func main(){
	a,_,_ = text()
}

交换变量

var a,b int = 1,2
a,b = b,a

go中交换变量非常简单,不需要借助中间变量或者调用函数

变量类型

常量

// 声明常量
const a = 1

特殊常量 iota

iota是go语言的常量计数器。在const关键字出现时被置为0,每次新增一个const将使iota技术一次。即使没有使用。

布尔

var ok bool = true

数字类型

var a int = 1
// 此外还有 int8 int 16 int32 int64 表示几位的整型
// uint为无符号整型,同样可以控制二进制位数
// 此外还有 byte rune uintptr 等类型

var c float32 = 1
var c float64 = 2
// go中没有 double 类型,float64 相当于是双精度浮点数 double

字符类型

var str string = "haha"

// go中同样可以用 + 号拼接字符串,如:
str += "xixi"

var c = 'A'
// go中没有char类型,实际上上面的c为int32类型,即通过ASCII码来映射字符

go中的string类型与Java相同,是不可改的。如果想操作字符串,则需要转换类型

s := "hello sysu"
tmp := []byte(s)
tmp[0] = 'a'
newStr = string(tmp)

// s=hello sysu
// newStr=aello sysu

go语言中不支持隐式类型转换,需要类型转换时,必须显示转换

a := 1
b := 2.0

b = (int)a

由于不支持隐式类型转换,因此go语言中,不支持int类型和bool类型的混合运算,必须转换至同一类型才能够进行运算

位运算符

go中的关系运算符,算术运算符,逻辑运算符,赋值运算符都与c++,java一致。

// 按位清除
&^

a := 60   //111100
b := 13   //001101

c := a&^b //110000   //48

// 其他的 & , | , ^ , <<, >> 与其它语言一致

按位清除的运算规则:

  • 如果第二个操作数的某一位是1,那么结果中的对应位会被清除为0。

  • 如果第二个操作数的某一位是0,那么结果中的对应位会保留第一个操作数的值。

输入与输出

// go中fmt包内具有输入输出的函数
// 输出
fmt.Println()   // 打印换行
fmt.Print()     // 打印
fmt.Printf()    // 格式化打印

// 输出
fmt.Scanf()
fmt.Scanln()
fmt.Scan()
// fmt.Scanln(&x,&y)

指针

// go中的指针,声明方式,使用方式都与c语言类似
var ptr *int = &a

// go中的空为 nil ,而非null
ptr = nil
  • go中的指针为了安全性和简化语言,不支持指针运算(如:ptr++)
  • go中具有垃圾回收机制,不需要手动释放指针

流程控制

// go中的if else语句不用小括号
if a>0 {
    fmt.Println("haha")
}

// go中的if else语句还支持在语句中赋值,再判断
if a,ok := hash[i];ok{
    // 如果ok为true,则会进入
    ·····
}

// switch
switch val {
case val1:
case val2:
default:
}

go语言中,case语句会自动防穿透,不需要像c++那样显式break结束。

使用 fallthrough 可以实现贯穿效果,无论下一个case条件是否满足,都会强制执行。

// for循环
for i:=0; i<len(nums); i++ {
    
}
// 或者类似于while
i:=0
for i<10 {
    i++
}

go语言中没有while关键字。break,continue功能一致

数组

// 一维数组
var arr [5]int

arr := [5]int{1,2,3,4,5}
arr := []int{1,2,3,4,5}

// 遍历方式
for i:=0;i<len(nums);i++{}
// 或者
for index,val := range nums{}

//多维数组
var matrix [2][3]int

matrix := [2][3]int{ {1,2,3},{4,5,6},}

// 动态添加元素
arr = append(arr,3)

切片操作

go中的数组也支持切片操作

arr := [5]int{1,2,3,4,5}
lmt.Println(arr[1:4])
// 输出2,3,4    切片范围为左闭右开

函数

// go语言函数定义格式如下
func name([parameter list]) [return_type]{}

// go语言中,函数参数是先参数,再类型
func text(a int){}

//特别的,go语言支持有多个返回值,例如:
func text()(int,int){
    return 100,200
}

// 可变参数
func text(nums ...int){}
// ...int 代表可变参数,可以接收任意个参数,但是可变参数必须是最后一个参数

// 提前确定返回变量
func add(a,b int)(sum int){
    if a <= 1 {
        return 0   // 相当于把0赋值给sum
    }
    sum = a + b
    return
}

关键字defer,可用于延迟函数的执行

func text1(){
    fmt.Println("hehe")
}
func text2(){
    fmt.Println("xixi")
}
func main(){
    defer text1()
    text2()
}
// 程序会先执行text2(),再执行text1()

如果有多个defer,则可以把他们当作栈的结构,先进的后出,后进的先出

go中的函数也是一个变量,可以进行赋值。可以将函数看作一个指针

func f1(a,b int){
    fmt.Println("haha")
}
func main(){
    var f2 = func(int,int)
    f2 = f1
}

匿名函数

// 不带名字的函数即为匿名函数
func(){
    fmt.Println("hello")
}

// 可以类似于lambda表达式进行函数赋值
f1 := func(a int){
    fmt.Println(a)
}

// 可以在函数后加上(),表示立即执行
func(){
    fmt.Print("haha")
}()
// 同样可以进行赋值,初始化等
a := func() int{
    return 1
}()

函数式编程

go语言中,函数可以作为参数进行传参

func main(){
    fmt.Println(oper(1,2,add))
}
func add(a,b int) int {
    return a+b
}
func oper(a,b int,f func(int,int) int) int {
    return f(a,b)
}

其中add被称为回调函数,oper称为高阶函数

结构体

结构体定义

// 定义方式
type Person struct{
    name string
    Age int
}
// Go中的结构体字段和方法的访问权限由命名方式决定。
// 如果首字母大写,则相当于public,可以在任意包访问
// 如果首字母小写,则相当于private,只能在定义包访问

// 对象的声明方式
// 直接初始化
p := Person{name: "Alice", Age: 30}
// 按字段声明顺序初始化
p := Person{"Alice", 30}
// 使用new关键字
p := new(Person)
// 部分初始化
p := Person{name: "Tom"}  // 未初始化的字段会赋予类型零值

go中结构体指针的使用方法跟java类似,不像c和c++那样用 -> 访问。统一用 . 访问字段和方法

结构体字段tag

go中支持为结构体字段打tag,使得在不同的数据交互时更加方便,例如:

var newPoll struct {
	Title   string   `json:"title" binding:"required"`
	Type    string   `json:"type" binding:"required"`
	Options []string `json:"options" binding:"required"`
}
  • json: 用于json数据的序列化和反序列化,进行映射
  • binding:“required” 表示该字段在请求中是必需的,如果请求中没有该数据,则会返回错误

除了上述的两种标签外,还有其他的常用标签

// xml
Name string `xml:"name"` 

// form
Username string `form:"username"`

// gorm
type User struct {
    ID   uint   `gorm:"primaryKey"`  // 主键
    Name string `gorm:"column:user_name"`  // 数据库列名为 "user_name"
    Age  int    `gorm:"default:18"`  // 默认值为 18
}

// yaml
Host string `yaml:"host"`

结构体中的方法

  • go的结构体没有构造函数。因为go中有垃圾回收机制,因此也没有析构函数
  • go的结构体方法分为
    • 值接收者方法,结构体的副本
    • 指针接收者方法,结构体本身
// 值接收者
func (p Person) SayHello(){
    fmt.Println("hello")
}

// 指针接收者
func (p *Person) SetName(name string){
    p.name = name
}
  • go中没有class关键字,因此结构体的方法通过在函数名前带有结构体对象或指针来区分(类似于py的sef)
  • 由于值接收者和指针接收者在方法集上不一致,以及出于安全性考虑,一个结构体的方法最好统一。要么全部都是值接收者,要么全部都是指针接收者

接口

// 定义形式
type 接口名 interface {
    方法名1(参数列表) 返回值列表
}

// 示例
type Person interface {
    Speak()
}

type Student struct {
    Name string
    Age string
}

func (st Student) Speak(){
    fmt.Println("haha")
}

在go中,接口的继承是隐式的,只要一个类实现了接口中定义的所有方法,就认为这个类实现了该接口,无需显示声明。

map

var m map[int]string      // 声明
m = make(map[int]string)  // 初始化
m[1] = "Tom"
fmt.Println("m :", m)

// 使用map装载json类型数据
mj := make(map[string]interface{})

// 对于固定类型的数据,不要使用map[string]interface{},最好使用临时struct。

// map的初始化,类似于json格式
hash := map[byte]int {
    'a':0,
    'b':0
}

基于go语言支持多返回值的特性,map的访问会返回两个值

  • 第一个返回值时key对应的value,如果不存在,则会返回对应类型的零值
  • 第二个返回值为bool类型,为该key是否存在。
m := make(hash[int]int)

m[1]=1
m[2]=2

// 可以只接收一个值,go会自动忽略第二个值
a := m[1]
// 也可以手动忽略第二个值
a,_ := m[1]
// 接收两个值,第二个返回值常常可以用于判断该键是否存在
a,ok := m[0]

并发编程

并发编程是一种编程范式,它允许程序中的多个任务看似同时执行(在单核处理器上是时间分片,在多核处理器上可以真正并行)。Go语言通过goroutine和channel提供了优雅的并发支持。

Go语言使用go关键字启动goroutine,而channel用于goroutine间的通信。

特点

  1. 轻量级线程:goroutine是轻量级的,由Go运行时管理,比操作系统线程更高效
  2. 并发执行:goroutine会并发执行,不阻塞主程序
  3. 简单语法:只需在函数调用前加go关键字即可

注意事项

  1. 主goroutine退出:如果主goroutine退出,所有其他goroutine也会立即终止
  2. 参数求值go语句会在当前goroutine中对参数进行求值
  3. 返回值:goroutine中的函数返回值会被忽略,需要通过channel等方式传递结果
  4. 调度顺序:goroutine的执行顺序是不确定的

示例:

func sum(s []int, c chan int) {
	sum := 0
	for _, v := range s {
		sum += v
	}
	c <- sum // 将结果发送到channel
}

func main() {
	s := []int{7, 2, 8, -9, 4, 0}
	c := make(chan int)
	go sum(s[:len(s)/2], c)
	go sum(s[len(s)/2:], c)
	x, y := <-c, <-c // 从channel接收
	fmt.Println(x, y, x+y)
}

每次接收操作(<-c)都会从channel获取一个新值

channel类型:

  • chan<- int:只发送channel
  • <-chan int:只接收channel

生产者与消费者模型

生产者-消费者模式是并发编程中一种经典的设计模式,用于解决不同执行速度的模块之间的协作问题。

工作流程: 生产者将数据放入缓冲区 -> 消费者从缓冲区取出数据 -> 当缓冲区满时,生产者等待 -> 当缓冲区空时,消费者等待

基础示例:

package main

import (
	"fmt"
	"time"
)

func producer(ch chan<- int) {
	for i := 0; i < 5; i++ {
		fmt.Printf("生产: %d\n", i)
		ch <- i // 发送数据到channel
		time.Sleep(time.Second) // 模拟生产耗时
	}
	close(ch) // 生产完毕,关闭channel
}

func consumer(ch <-chan int) {
	for v := range ch { // 循环读取channel直到关闭
		fmt.Printf("消费: %d\n", v)
		time.Sleep(2 * time.Second) // 模拟消费耗时
	}
}

func main() {
	ch := make(chan int, 3) // 带缓冲的channel,容量为3
	
	go producer(ch)
	consumer(ch)
}

关闭channel:只有发送方可以关闭channel,关闭后接收方仍可读取剩余数据。for range会在channel关闭后自动退出。

实际应用场景

  1. 日志处理系统(生产者生成日志,消费者处理日志)
  2. 消息队列系统
  3. 任务分发系统
  4. 数据流水线处理

Gin框架

go语言开发后端可以选择gin框架进行开发

// 使用go get命令安装Gin框架
go get -u github.com/gin-gonic/gin

// 并且需要导入相应包
import "github.com/gin-gonic/gin"

入门程序

package main

import "github.com/gin-gonic/gin"

func main() {
	// 创建一个新的Gin路由器
	r := gin.Default()

	// 定义一个简单的GET路由
	r.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "Hello Golang",
		})
	})

	// 启动HTTP服务器,监听8080端口
	err := r.Run(":8080")
	if err != nil {
		return
	}
}
  • c.JSON返回一个JSON格式的响应
  • gin.H是一种快捷方式,用于创建一个map[string]interface{}的JSON数据

除了c.JSON外,还有其他的响应格式向前端返回数据

// 返回字符串格式
c.String(200, "Hello Golang")

// 返回HTML格式
c.HTML(200, "index.html", gin.H{
    "title": "Home Page",
})

// 返回XML格式
c.XML(200, gin.H{
    "message": "Hello Golang",
})

// 返回文件
c.File("path/to/file.txt")

// 重定向
c.Redirect(301, "/new-location")

// 返回流式响应
c.Stream(func(w io.Writer) bool {
    fmt.Fprintf(w, "Chunk of data\n")
    return true // 继续发送
})

接收前端发送的数据

// 接收JSON数据
c.ShouldBindJSON(&user)

// 接收表单数据 使用 PostForm 或 ShouldBind 
name := c.PostForm("name")
email := c.PostForm("email")

c.ShouldBind(&user)

// 接收查询参数 使用 Query 或 DefaultQuery
// 形如:https://example.com/user?name=John&age=30
name := c.Query("name")
email := c.DefaultQuery("email", "default@example.com")

// 接收路径参数
// 形如:https://example.com/user/{id}
name := c.Param("id")

// 接收文件
file, err := c.FormFile("file")
if err != nil {
	c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
}

中间件

Logger

用于记录请求的相关日志信息

r := gin.New()
r.Use(gin.Logger())
  • 记录每个请求的路径、方法、处理时间等信息。

  • 可以记录请求的状态码和响应时间,方便开发者分析请求的性能和错误。

  • 有助于监控和调试,尤其是在生产环境中,查看请求日志可以帮助诊断问题。

Recovery

用于捕获运行时的panic错误,防止程序崩溃

r := gin.New()
r.Use(gin.Recovery())
  • 当程序发生panic时,Recovery中间件会捕获异常并防止应用崩溃。

  • 可以记录panic的错误信息,并恢复到正常的执行流程,通常会返回一个500的服务器错误响应。

  • 在生产环境中,使用Recovery中间件可以保证即使出现错误,服务仍然可以继续运行。

自定义中间件

Gin框架中提供了非常方便的自定义中间件形式

func MyLogger() gin.HandlerFunc {
	return func(c *gin.Context) {
		fmt.Println("Request received")
		c.Next()
	}
}

其中c.Next()是用来通知框架继续执行后续中间件的。它的作用是让请求处理流程继续向下传递

连接数据库

标准库 database/sql

连接数据库

import (
	"database/sql"

	"github.com/go-sql-driver/mysql"
)

func main() {
	// 构造 DSN(Data Source Name)
	dsn := "user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8&parseTime=True&loc=Local"

	// 连接数据库
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		log.Fatalf("数据库连接失败: %v", err)
	}
	// 检查连接是否正常
	if err := db.Ping(); err != nil {
		log.Fatalf("数据库 ping 失败: %v", err)
	}
	defer db.Close()
}

增删改查

// 查询
rows, err := db.Query("SELECT id, name FROM users WHERE age = ? AND city = ?", age, city)
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

var users []map[string]interface{}
for rows.Next() {
    var id int
    var name string
    // 使用 rows.Scan 扫描一行数据
    err := rows.Scan(&id, &name)
    if err != nil {
        log.Fatal(err)
    }
    users = append(users, map[string]interface{}{"id": id, "name": name})
}
  • rows.Next() 会遍历结果集中的每一行。
  • rows.Scan 用来扫描查询结果的一行数据,并将数据填充到 Go 变量中。

查询数据使用Query方法,需要返回数据。增删改使用Exec方法,适用于没有返回结果的SQL语句

// 插入
_, err := db.Exec("INSERT INTO users (name, age, city) VALUES (?, ?, ?)", "Alice", 25, "Shanghai")
if err != nil {
    log.Fatal(err)
}

// 更新
_, err := db.Exec("UPDATE users SET name = ? WHERE id = ?", "Bob", 1)

// 删除
_, err := db.Exec("DELETE FROM users WHERE id = ?", 1)

GORM

// 使用GORM前,需要先安装GORM及对应的数据库驱动
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql

连接示例

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

type User struct {
	ID   uint   `gorm:"primaryKey"`
	Name string `gorm:"size:255"`
}

func main() {
	// 构造 DSN
	dsn := "user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
	// 连接数据库
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		log.Fatalf("数据库连接失败: %v", err)
	}

	// 自动迁移(可选)
	db.AutoMigrate(&User{})
}

db.AutoMigrate(&User{})用于自动创建一个与User结构相同的数据库表

增删改查

// 查询,结果填充到users中
var users []User
result := db.Where("age = ? AND city = ?", 30, "Shanghai").Find(&users)

if result.Error != nil {
    log.Fatal(result.Error)
}

// 插入
user := User{Name: "Alice", Age: 25, City: "Shanghai"}
result := db.Create(&user)

// 更新
result := db.Model(&User{}).Where("id = ?", 1).Update("name", "Bob")
// db.Model(&User{}) 指定操作的目标模型是	User

// 删除
result := db.Where("id = ?", 1).Delete(&User{})

GROM中,指定SQL语句操作的数据库表

  1. 默认命名规则

默认映射规则:如果没有额外配置,GORM 会将结构体名称转换成小写,并自动加上复数形式作为表名。例如:如果你的结构体名称是 User,默认对应的表名会是 users

  1. 自定义表名,实现TableName()方法
type User struct {
    ID   uint
    Name string
}

// 明确指定 User 模型对应的表名为 "user"
func (User) TableName() string {
    return "user"
}
  1. 使用db.Table方法
// 明确指定查询 user 表的数据
result := db.Table("user").Find(&users)

原生SQL与GORM对比

特性GORM原生 SQL
开发效率高(自动生成 SQL)低(需要手写 SQL)
代码可读性高(结构体和方法调用)低(SQL 字符串拼接)
灵活性中等(受限于 ORM 功能)高(完全控制 SQL)
学习成本中等(需要学习 GORM API)低(直接使用 SQL)
适用场景快速开发、中小型项目复杂查询、高性能场景

连接Redis

通常使用go-redis来连接Redis

package main

import (
	"context"
	"github.com/go-redis/redis/v8"
)

var ctx = context.Background()

func main() {
	// 创建 Redis 客户端
	rdb := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "", 
		DB:       0,  // 默认数据库
	})

	// 检查连接
	_, err := rdb.Ping(ctx).Result()
	if err != nil {
		log.Fatalf("Redis 连接失败: %v", err)
	}
}

统一管理连接信息

在Gin框架中同样可以使用配置文件的方式来统一管理连接信息,便于维护

yml文件

database:
  mysql:
    dsn: "username:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
  redis:
    addr: "localhost:6379"
    password: ""
    db: 0

读取配置文件的代码,需要使用viper

// 导入viper库
go get github.com/spf13/viper

confg包

package config

import (
    "github.com/spf13/viper"
    "log"
)

func InitConfig() {
    viper.SetConfigName("config") // 配置文件名称(无扩展名)
    viper.SetConfigType("yml")   // 配置文件类型
    viper.AddConfigPath(".")     // 配置文件路径
    if err := viper.ReadInConfig(); err != nil {
        log.Fatalf("Error reading config file: %v", err)
    }
}

func GetMySQLDSN() string {
    return viper.GetString("database.mysql.dsn")
}

func GetRedisConfig() (string, string, int) {
    return viper.GetString("database.redis.addr"),
           viper.GetString("database.redis.password"),
           viper.GetInt("database.redis.db")
}

database包

package database

import (
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "github.com/go-redis/redis/v8"
    "context"
    "log"
)

var (
    DB  *gorm.DB
    RDB *redis.Client
)

func InitMySQL(dsn string) {
    var err error
    DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatalf("Failed to connect to MySQL: %v", err)
    }
    log.Println("Connected to MySQL!")
}

func InitRedis(addr, password string, db int) {
    RDB = redis.NewClient(&redis.Options{
        Addr:     addr,
        Password: password,
        DB:       db,
    })

    ctx := context.Background()
    _, err := RDB.Ping(ctx).Result()
    if err != nil {
        log.Fatalf("Failed to connect to Redis: %v", err)
    }
    log.Println("Connected to Redis!")
}

主函数

package main

import (
    "github.com/gin-gonic/gin"
    "your_project/config"
    "your_project/database"
    "your_project/handlers"
)

func main() {
    // 初始化配置
    config.InitConfig()

    // 初始化数据库连接
    database.InitMySQL(config.GetMySQLDSN())
    database.InitRedis(config.GetRedisConfig())

    // 创建 Gin 实例
    r := gin.Default()

    // 注入数据库连接到路由或服务层
    handlers.InitHandlers(r, database.DB, database.RDB)

    // 启动服务
    r.Run(":8080")
}