第一天我们已经熟悉了一些基础概念可以写提供给客户端一个接口来来通过名字预测性别了, 第二天我们学习一下写 Golang 的常见编程模式
OOP
OOP 是我们写业务的核心设计模式, Golang 是一个 “现代” 的语言,为什么打个引号呢?因为 Golang没有传统的类和继承的概念 😅,但它提供了一些机制来实现面向对象编程的特性。
对象
假设我们写一个 user 对象, 有一些属性,比如 username,password, 有一个登录方法, TS 示例如下,贴这个例子时候在感叹 TS 和 Python, Ruby 开发起应用太“快”了。。。
class User {
name: string;
age: number;
email: string;
password: string;
constructor(name: string, age: number, email: string, password: string) {
this.name = name;
this.age = age;
this.email = email;
this.password = password;
}
login(): void {
console.log(`User ${this.name} logged in.`);
}
}
那么在 golang 中如何定义一个对象呢?通过用 struct
关键字定义一个结构体,里面放一些属性,需要注意的是,放方法时候很特殊,方法不是放在结构体肚子里,而是放外面,通过一个叫做 receiver 来将一个普通方法和结构体关联在一起
type User struct {
name string
age int
email string
password string
}
func (u User) Login() {
fmt.Println("User", u.name, "logged in.")
}
Line 8 的 (u User)
就是一个 receiver,定义了这个对象后我们就可以去 new, 去实例化了。但是 golang 中不需要像 TS 那样显式的 new, 可以直接赋值并实例化
u := User{name: "Evle", age: 30, email: "evle@example.com", password: "888"}
u.Login() // 输出:User John logged in.
目前的这个 Login 方法, 只是将用户的名字输出, 不对这个实例产生影响,如果我们要提供一个编辑用户密码的方法,我们这个 Reciver 要改成 指针 即 (u *User)
func (u *User) ChangePassword(newPassword string) {
u.password = newPassword
fmt.Println("User", u.name, "changed password.")
} // 调用示例
u.ChangePassword("newpassword") // 输出:User John changed password.
为什么要改成指针呢?这涉及到 传值 和 传引用 的概念,仔细听,u User
和 u *User
的区别。这个 u
代表的是我们这个 instance, 名字是 Evle, age 是 30, 如果使用不加 * 就是传值,也就是,是一份拷贝,我们对份拷贝数据的修改不会影响这个实例
func (u User) ChangeAge(newAge int) {
u.age = newAge
所以这样修改 age 是不会生效的,因为没有修改到 这个 Evle 这个instance, 改的只是一个临时拷贝,当这个函数 scope 运行结束时,那临时修改的这个拷贝就会消失,即什么都没改。
那换成 u *User
就是传引用,也就是这个 u 就是 Evle, 对 u 的操作都会影响 Evle 这个实例,比如上面的例子修改密码
func (u *User) ChangePassword(newPassword string) {
u.password = newPassword
如果这里有对 constructor
有热爱的同学,可以自行写一个 constructor 函数来完成类的实例化
func NewUser(name string, age int, email string, password string) User
{
return User{name: name, age: age, email, password}
}
evle := NewUser("Evle", 30)
接口
还是先用 TS 举例, 我们有 2个 数据库, MySQL 和 Postgres,这两个数据库都有 连接,查询,断开 3个方法。我们要怎么写呢?我们需要定义一个 database 接口,然后让 MySQL 和 Postgres 这两个类实现这个接口,实现完我们就可以根据实际情况 new 了,一个典型的工厂模式。
interface Database {
connect(): void;
query(query: string): Result;
close(): void;
}
class MySQL implements Database {
host: string;
port: number;
username: string;
password: string;
constructor(host: string, port: number, username: string, password: string) {
this.host = host;
this.port = port;
this.username = username;
this.password = password;
}
connect(): void {
// 连接到MySQL数据库
}
query(query: string): Result {
// 执行查询并返回结果
return new Result();
}
close(): void {
// 关闭与MySQL数据库的连接
}
}
在 golang 中定义这个接口和实现这个接口呢?先定义接口用 type
type Database interface {
Connect() error
Query(query string) (Result, error)
Close() error
}
然后定义一个类来实现这个接口
type MySQL struct {
host string
port int
username string
password string
}
func (m MySQL) Connect() error {
// Connect to MySQL database
return nil
}
func (m MySQL) Query(query string) (Result, error) {
// Execute the query and return results
return Result{}, nil
}
func (m MySQL) Close() error {
// Close the connection to MySQL database
return nil
}
继承
刚才说了 golang 没有所谓的 extend 关键字去继承,它是通过简单的嵌套实现继承的,用刚才的 user 举例,我们现在想创建一个 admin 对象, 它比普通用户多一个 role 的角色,那我们可以直接定义一个 admin 的结构体,将 user 嵌套进去
type User struct {
name string
age int
email string
password string
}
func (u User) Login() {
fmt.Println("User", u.name, "logged in.")
}
type Admin struct {
User
role string
}
func main() {
admin := Admin{
User: User{
name: "Evle",
age: 30,
email: "evle@example.com",
password: "password123",
},
role: "administrator",
}
admin.Login()
fmt.Println("Admin role:", admin.role)
}
当然你也可以重写 Login 方法
func (a Admin) Login() {
fmt.Println("Admin", a.name, "logged in.")
}
并发编程
在 Golang 里面有个关键字 go
,光看名字就不平凡, go 是用来处理并发任务的,比如并发发送请求之类的。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(2)
var sharedData int
go func() {
defer wg.Done()
// 第一个并发任务
sharedData = 10
fmt.Println("第一个并发任务设置了共享数据:", sharedData)
}()
go func() {
defer wg.Done()
// 第二个并发任务
fmt.Println("第二个并发任务读取了共享数据:", sharedData)
}()
wg.Wait()
fmt.Println("并发任务执行完毕")
}
Line 14 和 Line 21 分别起了 2 个并发任务,和定义普通函数一样,前面用 go 修饰一下就可以。 Line 9,10 我们通过 sync.WaitGroup
做了一个并发控制,也就是让我们的主线程等待着(Line 27),直到 2 个并发任务都执行完,也就是每个任务 wg.Done
后,主线程再继续执行。
注意:如果在每个并发任务的匿名函数中不调用 wg.Done()方法,sync.WaitGroup将永远等待,主程序将无法继续执行。这会导致程序被阻塞住,无法正常结束。
既然是多个任务并行,那就绕不开一个问题 多任务之间如何共享数据?,我们上面的例子中是使用 共享变量来共享数据的,但是它在并发编程的场景里面需要:避免数据竞态条件,也就是说共享变量的读写顺序可能失控。那怎么做呢? 可以使用 sync.Mutex
来实现互斥锁
func main() {
var wg sync.WaitGroup
wg.Add(2)
var mu sync.Mutex
var sharedData int
go func() {
defer wg.Done()
// 第一个并发任务
mu.Lock()
sharedData = 10
mu.Unlock()
fmt.Println("第一个并发任务设置了共享数据:", sharedData)
}()
go func() {
defer wg.Done()
// 第二个并发任务
mu.Lock()
defer mu.Unlock()
fmt.Println("第二个并发任务读取了共享数据:", sharedData)
}()
wg.Wait()
fmt.Println("并发任务执行完毕")
}
在这个例子中,我们创建了一个互斥锁mu
,并在每个并发任务中使用mu.Lock()
来获取锁,使用mu.Unlock()
来释放锁。通过在访问共享变量之前获取锁,然后在访问完成后释放锁,可以确保同一时间只有一个goroutine可以访问共享变量。
需要注意的是,在获取锁之后,需要在适当的时候使用defer
语句来调用mu.Unlock()
,以确保即使在函数发生错误或提前返回时,互斥锁也能被正确地释放。
通过使用互斥锁,我们可以避免竞态条件和数据访问冲突,确保共享变量的安全访问。
除了共享变量外, golang 还提供了一个 channel 来共享数据,在本例中,通过 make(chain int)
创建一个共享数字的 channel
func main() {
var wg sync.WaitGroup
wg.Add(2)
sharedData := make(chan int)
go func() {
defer wg.Done()
// 第一个并发任务
sharedData