1. 为什么要学go?

对于有C、C++、Java等高级语言基础的同学,golang的上手很快,学习曲线很友好。

同时golang还有以下特点:

  • 丰富的标准库
  • 完整的工具链,用于单元测试、性能测试等
  • 静态编译
  • 快速编译,增量编译,静态编译速度远快于C++
  • 垃圾回收、类似java,无需考虑内存释放

为什么很多公司用go,比如字节、腾讯等

  • c++不适合web开发
  • python太慢
  • 学习成本低
  • 现成的rpc框架等
2. 相关资料 3. 基本语法
  1. 强类型语言:字符串、整数、布尔

  2. 变量声明

var 变量名字 类型 = 表达式
var s string = "xxx"
var s =”xxx” (自动推导类型)
s := “xxx”

常量加一个const,无需声明

  1. if-else

if-else的if后面没有括号,且大括号必须要是在if或else的同一行

  1. for循环

也是没有括号,也必须把大括号的左括号写在for同一行

  1. switch

不需要括号,默认不需要加break也不会跑其他的case分支

switch也可以替代if else写一句话的分支,而不需要仅对一个变量进行switch case的分析

  1. 数组

定长,用的少

切片:变长

用make初始化生成,用append在尾部追加元素后赋值给原切片,copy函数拷贝两个切片(前des,后src),支持向python一样访问切片中的部分元素

  1. 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的元素

  1. range

常和for一起使用

  1. 函数

func声明

变量类型后置

可返回多个变量

  1. 指针

类似于c/c++

n int
n *int
*n +=2
n :=5
add2(&n)
  1. 结构体
type sname struct{
    name string
    password string
}

a :=()

可以部分赋值,可以不显示写出struct内的元素的名称

结构体方法:传结构体指针/值拷贝,类似于类内成员函数

  1. 错误处理
import{
    “errors”
}
  1. 字符串处理
import {
    “strings”
}

strings.Count()
strings.Index()

字符串格式化

fmt.Printf(”…”)同c语言的printf

  1. JSON处理
import{
    “encoding/json”
}

json.Marshal() 序列化
json.Unmarshal() 反序列化
  1. 时间处理
import{
    “time”
}

now :=time.Now()
  1. 数字解析

strconv包

strconv.ParseFloat(”123141”,10,64)

10为10进制,64为返回64位

  1. 进程信息
os
os/exec
os.Getenv os.Setenv 获得环境变量,设置环境变量
4. MVC项目解析——社区后台

4.1 需求分析

总体需求:

完成一个简单的聊天社区后台,提供一些接口,能够让用户根据指定的话题内容进行发帖讨论(类似于豆瓣的每日话题),用户也可以看到某个话题下的所有帖子。

并不需要做前端界面,只提供后台接口就可以,但是为了便于扩展和维护,希望项目使用mvc结构,能够从本地文件村粗(以后)扩展为分布式数据库存储。

但是目前只是需要本地文件存储

具体分析

  1. 实现两个路由,一个get请求(获取指定topic id的topic title及它的所有回帖列表);一个post请求(指定发帖的topic id和发帖的内容,后台生成一条新的帖子记录)
  2. 用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中的性能测试工具。