对于有C、C++、Java等高级语言基础的同学,golang的上手很快,学习曲线很友好。
同时golang还有以下特点:
- 丰富的标准库
- 完整的工具链,用于单元测试、性能测试等
- 静态编译
- 快速编译,增量编译,静态编译速度远快于C++
- 垃圾回收、类似java,无需考虑内存释放
为什么很多公司用go,比如字节、腾讯等
- c++不适合web开发
- python太慢
- 学习成本低
- 现成的rpc框架等
-
强类型语言:字符串、整数、布尔
-
变量声明
var 变量名字 类型 = 表达式
var s string = "xxx"
var s =”xxx” (自动推导类型)
s := “xxx”
常量加一个const,无需声明
- if-else
if-else的if后面没有括号,且大括号必须要是在if或else的同一行
- for循环
也是没有括号,也必须把大括号的左括号写在for同一行
- switch
不需要括号,默认不需要加break也不会跑其他的case分支
switch也可以替代if else写一句话的分支,而不需要仅对一个变量进行switch case的分析
- 数组
定长,用的少
切片:变长
用make初始化生成,用append在尾部追加元素后赋值给原切片,copy函数拷贝两个切片(前des,后src),支持向python一样访问切片中的部分元素
- map
m := make(map[string]int)
key为string类型,value为int类型
golang中的map完全无序
可以通过 v, ok =mp[”somekey”]
来获取key=“somekey”是否存在(由ok返回,bool类型),如果存在则其value是什么(由v返回,string类型)
delete函数删除对应key的元素
- range
常和for一起使用
- 函数
func声明
变量类型后置
可返回多个变量
- 指针
类似于c/c++
n int
n *int
*n +=2
n :=5
add2(&n)
- 结构体
type sname struct{
name string
password string
}
a :=()
可以部分赋值,可以不显示写出struct内的元素的名称
结构体方法:传结构体指针/值拷贝,类似于类内成员函数
- 错误处理
import{
“errors”
}
- 字符串处理
import {
“strings”
}
strings.Count()
strings.Index()
字符串格式化
fmt.Printf(”…”)同c语言的printf
- JSON处理
import{
“encoding/json”
}
json.Marshal() 序列化
json.Unmarshal() 反序列化
- 时间处理
import{
“time”
}
now :=time.Now()
- 数字解析
strconv包
strconv.ParseFloat(”123141”,10,64)
10为10进制,64为返回64位
- 进程信息
os
os/exec
os.Getenv os.Setenv 获得环境变量,设置环境变量
4. MVC项目解析——社区后台
4.1 需求分析
总体需求:
完成一个简单的聊天社区后台,提供一些接口,能够让用户根据指定的话题内容进行发帖讨论(类似于豆瓣的每日话题),用户也可以看到某个话题下的所有帖子。
并不需要做前端界面,只提供后台接口就可以,但是为了便于扩展和维护,希望项目使用mvc结构,能够从本地文件村粗(以后)扩展为分布式数据库存储。
但是目前只是需要本地文件存储
具体分析
- 实现两个路由,一个get请求(获取指定topic id的topic title及它的所有回帖列表);一个post请求(指定发帖的topic id和发帖的内容,后台生成一条新的帖子记录)
- 用mvc模式去实现它,项目分为controller-service-repository结构
4.2 数据结构
type Topic struct {
Id int64 `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
CreateTime int64 `json:"create_time"`
}
type Post struct {
Id int64 `json:"id"`
ParentId int64 `json:"parent_id"`
Content string `json:"content"`
CreateTime int64 `json:"create_time"`
}
4.3 细节分析
Dao单例模式
单例模式是一种非常常用的设计模式,它用于当项目中只需要一个对象实例时(或者是每个对象初始化的开销都比较大),例如在数据库连接(Model/Repository层)中,数据库的连接对象只需要一个实例,此后代码中所有用到数据库的连接对象时,都只调用这一个实例即可。
在go中,提供了sync.once方法创建一个单例模式的对象,配合.Do函数,进行对象初始化。
饿汉模式与懒汉模式
在项目中,初始化dao用的是饿汉模式,只要调用NewPostDapInstance函数就创建一次,此后都只返回实例指针,但是其实也并非典型饿汉(相比于C++或者Java中有明确类初始化概念而言)。
var (
postDao *PostDao
postOnce sync.Once
)
func NewPostDaoInstance() *PostDao {
postOnce.Do(
func() {
postDao = &PostDao{}
})
return postDao
}
饿汉式创建
public class SingletonEH {
/**
*是否 Lazy 初始化:否
*是否多线程安全:是
*实现难度:易
*描述:这种方式比较常用,但容易产生垃圾对象。
*优点:没有加锁,执行效率会提高。
*缺点:类加载时就初始化,浪费内存。
*它基于 classloder 机制避免了多线程的同步问题,
* 不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,
* 在单例模式中大多数都是调用 getInstance 方法,
* 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,
* 这时候初始化 instance 显然没有达到 lazy loading 的效果。
*/
private static SingletonEH instance = new SingletonEH();
private SingletonEH (){}
public static SingletonEH getInstance() {
System.out.println("instance:"+instance);
System.out.println("加载饿汉式....");
return instance;
}
}
懒汉式创建
public class SingletonLH {
/**
*是否 Lazy 初始化:是
*是否多线程安全:否
*实现难度:易
*描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
*这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。
*/
private static SingletonLH instance;
private SingletonLH (){}
public static SingletonLH getInstance() {
if (instance == null) {
instance = new SingletonLH();
}
return instance;
}
}
而在golang中,用sync.Once 是使方法只执行一次的对象实现,作用与 init 函数类似。但也有所不同。
- init 函数是在文件包首次被加载的时候执行,且只执行一次
- sync.Onc 是在代码运行中需要的时候执行,且只执行一次
多个goroutine也只会调用一次once.Do(),当某个goroutine调用到了之后,其他的就不会再调用,关于sync.Once的更多细节:你真的了解 sync.Once 吗 | Go 技术论坛 (learnku.com)
gin与路由
项目中的web框架,用了go中一个非常常用的叫做gin, gin-gonic/gin GitHub源码
net/http
在代码server.go中,使用gin创建两个路由,一个用于根据topicId查询话题及帖子,一个用于上传post
func main() {
if err := Init("./data/"); err != nil {
os.Exit(-1)
}
r := gin.Default()
r.GET("/community/page/get/:id", func(c *gin.Context) {
topicId := c.Param("id")
data := cotroller.QueryPageInfo(topicId)
c.JSON(200, data)
})
r.POST("/community/page/post", func(c *gin.Context) {
topicId, _ := c.GetPostForm("topicId")
content, _ := c.GetPostForm("content")
data := cotroller.PublishPost(topicId, content)
c.JSON(200, data)
})
err := r.Run()
if err != nil {
return
}
}
("./data")
f, err := os.OpenFile("./data/post", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return err
}
defer f.Close()
marshal, _ := json.Marshal(post)
if _, err = f.WriteString(string(marshal) + "\n"); err != nil {
return err
}
改成sql操作
// db_init.go
package repository
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var db *gorm.DB
func Init() error {
var err error
dsn := "root:981106@tcp(127.0.0.1:3306)/community?charset=utf8mb4&parseTime=True&loc=Local"
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
return err
}
// repository/post.go
func (*PostDao) QueryPostById(id int64) (*Post, error) {
var post Post
err := db.Where("id = ?", id).Find(&post).Error
if err == gorm.ErrRecordNotFound {
return nil, nil
}
if err != nil {
util.Logger.Error("find post by id err:" + err.Error())
return nil, err
}
return &post, nil
}
func (*PostDao) CreatePost(post *Post) error {
if err := db.Create(post).Error; err != nil {
util.Logger.Error("insert post err:" + err.Error())
return err
}
return nil
}
MVC模式与本项目中的具体实现
MVC模式是一种大名鼎鼎的软件架构模式,通常来说分为Model-View-Controller,它们从存储、控制、人机交互界面一层层传递需求,没有隔层调用,而是相邻层调用,能够更好地架构软件项目,把不同的开发层次区分出来,给不同的人进行开发
MVC的一种具体实现思路是:Dao层、Servie层、Controller层、View层。这里由于用go写后端,不涉及界面,因此View层,不讨论,只讨论前三层。首先看一个总体定义,来自MVC模式&DAO,Service,Controller、View层级理解_freeNeasy的博客
DAO层: DAO层主要是做数据持久层的工作,负责与数据库(文件)进行联络的一些任务都封装在此,DAO层的设计首先是设计DAO的接口,然后在Spring的配置文件中定义此接口的实现类,然后就可在模块中调用此接口来进行数据业务的处理,而不用关心此接口的具体实现类是哪个类,显得结构非常清晰,DAO层的数据源配置,以及有关数据库连接的参数都在Spring的配置文件中进行配置。
Service层: Service层主要负责业务模块的逻辑应用设计。同样是首先设计接口,再设计其实现的类,接着再Spring的配置文件中配置其实现的关联。这样我们就可以在应用中调用Service接口来进行业务处理。Service层的业务实现,具体要调用到已定义的DAO层的接口,封装Service层的业务逻辑有利于通用的业务逻辑的独立性和重复利用性,程序显得非常简介。
Controller层: Controller层负责具体的业务模块流程的控制,在此层里面要调用Serice层的接口来控制业务流程,控制的配置也同样是在Spring的配置文件里面进行,针对具体的业务流程,会有不同的控制器,我们具体的设计过程中可以将流程进行抽象归纳,设计出可以重复利用的子单元流程模块,这样不仅使程序结构变得清晰,也大大减少了代码量。**
简要概括下,Dao会直接与存储设备进行交互,包括增删改查,并暴露一个Dao接口或实例给上层调用;Service层会利用Dao层暴露的实例去操作数据库,它会做一些例如检查参数合法性、打包参数为一个struct、调用Dao层去增删改查这个struct,同时Service会暴露一些函数留给Controller层使用;Controller层负责总体的业务逻辑,在Controller层中的功能函数里,实际上是调用Service层对应的函数(具体实现)+Controller的错误/异常处理。
根据以上概括的原则,看一下我项目中写出的分层代码,以发布帖子这个功能为例
// server.go
func main() {
if err := Init("./data/"); err != nil {
os.Exit(-1)
}
r := gin.Default()
r.POST("/community/page/post", func(c *gin.Context) {
topicId, _ := c.GetPostForm("topicId")
content, _ := c.GetPostForm("content")
data := cotroller.PublishPost(topicId, content)
c.JSON(200, data)
})
err := r.Run()
if err != nil {
return
}
}
// controller/publish_post.go
func PublishPost(parentIdStr string, content string) *PostResponse {
parentId, _ := strconv.ParseInt(parentIdStr, 10, 64)
postId, err := service.PublishPost(parentId, content)
if err != nil {
return &PostResponse{
Code: -1,
Msg: err.Error(),
}
}
return &PostResponse{
Code: 0,
Msg: "success",
Data: map[string]int64{
"PostId": postId,
},
}
}
// service/publish_post.go
func PublishPost(topicId int64, content string) (int64, error) {
fmt.Println("service Publish Post")
return NewPublishPostFlow(topicId, content).Do()
}
func NewPublishPostFlow(topicId int64, content string) *PublishPostFlow {
return &PublishPostFlow{
content: content,
topicId: topicId,
}
}
type PublishPostFlow struct {
content string
topicId int64
postId int64
}
func (f *PublishPostFlow) Do() (int64, error) {
if err := f.checkParam(); err != nil {
return 0, err
}
if err := f.publish(); err != nil {
return 0, err
}
return f.postId, nil
}
func (f *PublishPostFlow) publish() error {
post := &repository.Post{
ParentId: f.topicId,
Content: f.content,
CreateTime: time.Now().Unix(),
}
id, err := generateIdBySnowFlake(100, 100)
if err != nil {
return err
}
post.Id = id
if err := repository.NewPostDaoInstance().InsertPost(post); err != nil {
return err
}
f.postId = post.Id
return nil
}
func (f *PublishPostFlow) checkParam() error {
// ...
}
func (f *PublishPostFlow) generateIdBySnowFlake(machinedId int64, datacenterId int64) (int64, error) {
// ...
}
// repository/post.go
func (*PostDao) InsertPost(post *Post) error {
f, err := os.OpenFile("./data/post", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return err
}
defer f.Close()
marshal, _ := json.Marshal(post)
if _, err = f.WriteString(string(marshal) + "\n"); err != nil {
return err
}
rwMutex.Lock()
postList, ok := postIndexMap[post.ParentId]
if !ok {
postIndexMap[post.ParentId] = []*Post{post}
} else {
postList = append(postList, post)
postIndexMap[post.ParentId] = postList
}
rwMutex.Unlock()
return nil
}
// data/post
{"id":1,"parent_id":1,"content":"content1","create_time":1650437616}
{"id":2,"parent_id":1,"content":"content2","create_time":1650437617}
{"id":3,"parent_id":1,"content":"content3","create_time":1650437618}
总结
综上我们就完成了一个简单的go demo,完整代码地址见本文章最上面,当然go lang有丰富的标准库与第三方库、静态编译提供更高的性能、配套的性能测试和优化工具,这些知识点都需要在实践中去学习训练,本文章只是记录了一次golang的入门初探。下一篇会分析一下golang中的性能测试工具。