golang开发技巧

使用gofmt

重要的话说3遍
使用gofmt
使用gofmt
使用gofmt
gofmt是golang提供的代码格式化工具,整个团队使用,就不需要做代码风格审查了

协程使用规范

将并发留给调用者

// ListDirectory returns the contents of dir.
func ListDirectory(dir string) ([]string, error)

// ListDirectory returns a channel over which
// directory entries will be published. When the list
// of entries is exhausted, the channel will be closed.
func ListDirectory(dir string) chan string

这两个函数显著的区别是,第一个例子读取目录到切片,然后将整个切片返回,否则如果有问题则返回一个错误。这是同步发生的,ListDirectory的调用者将被阻塞直到整个目录被读完。依赖于目录有多大,这个过程可能持续很长时间,也可能因为构建一个目录条目名称的切片,而分配大量的内存。

看第二个例子。这更一个更像Go,ListDirectory返回了一个传输目录条目的通道,当通道关闭时,表明没有更多目录条目了。由于通道信息发生在ListDirectory返回之后,ListDirectory内部可能开启了一个协程。

通道版本的ListDirectory还有两个进一步的问题:

  1. 通过使用通道的关闭作为没有更多项目要处理的信号,ListDirectory在中途遇到错误就无法告知调用者返回的集合是不完整的。调用者也无法区分空目录和一读取就产生错误的情况,这两种结果对于ListDirectory返回的通道来说都是立即关闭。
  2. 调用者必须持续读取通道的内容直到通道关闭,因为这是让调用者知道协程已经结束的唯一办法。这是对ListDirectory的使用的一个严重限制。调用者必须花时间从通道读取数据,哪怕调用者已经接收到它想要的信息。就需要使用大量内存的中型到大型目录而言,它可能更有效,但这种方法并不比原始的基于切片的方法快。
    以上两个实现中的问题,其解决方案是使用回调。一个在每个目录条目上执行的函数。
func ListDirectory(dir string, fn func(string))

最佳实践:filepath.Walk

总结:如果你的函数开启了一个协程,那么你必须给调用者提供一个停止协程的途径。将异步执行函数的决策留给该函数的调用者通常更容易

不要启动一个永不停止的协程

package main

import (
	"fmt"
	"net/http"
	_ "net/http/pprof"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
		fmt.Fprintln(resp, "Hello, QCon!")
	})
	go http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux) // debug
	http.ListenAndServe("0.0.0.0:8080", mux)                       // app traffic
}

上述代码优化后如下

func serveApp() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
		fmt.Fprintln(resp, "Hello, QCon!")
	})
	http.ListenAndServe("0.0.0.0:8080", mux)
}

func serveDebug() {
	http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux)
}

func main() {
	go serveDebug()
	serveApp()
}

通过将 serveApp 和 serveDebug 的功能放到到各自的函数中,我们把他们从 main.main 分离出来。我们照样遵循了上面的建议,把serveApp和serveDebug 的并发性留给了调用者

但是这个程序有一些可操作性上的问题。如果serveApp返回则main.main会返回并导致程序关闭,最终由您正在使用的任何进程管理器重新启动。
正如函数的并发性留给调用者一样,应用应该将状态监视、重启留给程序的唤起者。不要让你的应用程序担负重启自身的责任,这是一个最好从应用程序外部处理的过程。

func serveApp() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
		fmt.Fprintln(resp, "Hello, QCon!")
	})
	if err := http.ListenAndServe("0.0.0.0:8080", mux); err != nil {
		log.Fatal(err)
	}
}

func serveDebug() {
	if err := http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux); err != nil {
		log.Fatal(err)
	}
}

func main() {
	go serveDebug()
	go serveApp()
	select {}
}

现在我们通过必要时调用 log.Fatal 来检查 serverApp 和 serveDebug 从 ListenAndServe 返回的错误。由于两个处理器都是在协程中运行,我们使用 select{} 来阻塞主协程。
这种方法存在许多问题:

如果 ListenAndServe 返回一个 nil,log.Fatal不会被调用,则对应的 HTTP 服务会停止,并且应用程序不会退出。
log.Fatal 会调用 os.Exit无条件终止进程,defer不会被调用,其他协程不会被通知关闭,应用程序会停止。这会使得为这些函数编写测试用例变得很困难。

只在 main.main 或 init 函数里使用 log.Fatal

们需要的是,把任何错误都传回协程的发起者,以便于我们弄清楚为什么协程会停止,并且可以干净地关闭进程。

func serveApp() error {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
		fmt.Fprintln(resp, "Hello, QCon!")
	})
	return http.ListenAndServe("0.0.0.0:8080", mux)
}

func serveDebug() error {
	return http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux)
}

func main() {
	done := make(chan error, 2)
	go func() {
		done <- serveDebug()
	}()
	go func() {
		done <- serveApp()
	}()

	for i := 0; i < cap(done); i++ {
		if err := <-done; err != nil {
			fmt.Println("error: %v", err)
		}
	}
}

可以使用一个通道来收集协程返回的状态。通道的大小与我们要管理的协程数一致,从而使得向 done 通道发送状态时不会被阻塞,否则这将阻塞协程的关闭,导致泄漏。

由于没有办法安全地关闭 done 通道,我们不能使用 for range 循环通道知道所有协程都上报了信息,因此我们循环开启协程的次数,这也等于通道的容量。

现在我们有办法等待协程干净地退出,并且记录发生的日志。我们所需的仅仅是将一个协程的关闭信号,通知到其他协程而已。

其结果是,通知一个http.Server关闭这事被引入进来。所以我将这个逻辑转换为辅助函数。serve帮助我们持有一个地址和一个http.Handler,类似http.ListenAndServe 以及一个用于触发 Shutdown 方法的 stop 通道。

func serve(addr string, handler http.Handler, stop <-chan struct{}) error {
	s := http.Server{
		Addr:    addr,
		Handler: handler,
	}

	go func() {
		<-stop // wait for stop signal
		s.Shutdown(context.Background())
	}()

	return s.ListenAndServe()
}

func serveApp(stop <-chan struct{}) error {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
		fmt.Fprintln(resp, "Hello, QCon!")
	})
	return serve("0.0.0.0:8080", mux, stop)
}

func serveDebug(stop <-chan struct{}) error {
	return serve("127.0.0.1:8001", http.DefaultServeMux, stop)
}

func main() {
	done := make(chan error, 2)
	stop := make(chan struct{})
	go func() {
		done <- serveDebug(stop)
	}()
	go func() {
		done <- serveApp(stop)
	}()

	var stopped bool
	for i := 0; i < cap(done); i++ {
		if err := <-done; err != nil {
			fmt.Println("error: %v", err)
		}
		if !stopped {
			stopped = true
			close(stop)
		}
	}
}

现在,每当我们从 done 通道接收到一个值,就关闭stop通道,从而导致所有等待在这个通道上的协程关闭http.Server。这将导致所有剩余的ListenAndServe协程返回。一旦我们启动的协程停止,main.main 便返回继而进程干净地停止了。

字符串拼接

少量字符串拼接效率最高的是+使用也最简单,效率最差的是fmt.Sprint,但是每次+操作会导致内存重新分配,这时builder的优势就展现出来了。

  1. +连接适用于短小的、常量字符串(明确的,非变量),因为编译器会给我们优化。
  2. Join是比较统一的拼接,不太灵活
  3. fmt和buffer基本上不推荐
  4. builder从性能和灵活性上,都是上佳的选择。

不鼓励使用 nil 作为参数

在函数签名中不要混用可为 nil 和不可为 nil 的参数

http.ListenAndServe("0.0.0.0:8080", nil)

http.ListenAndServe("0.0.0.0:8080", http.DefaultServeMux)

两个方式都做完全一样的事情。

这种 nil的行为是病毒式的。在http包中同样有个http.Serve帮助类,您可以合理地想象 ListenAndServe 是这样建立的:

func ListenAndServe(addr string, handler Handler) error {

l, err := net.Listen("tcp", addr)

if err != nil {

return err

}

defer l.Close()

return Serve(l, handler)

}

因为ListenAndServe允许调用者为第二个参数传递nil,所以http.Serve也支持这种行为。事实上,http.Serve 是“当 handler 为 nil,则使用 DefaultServeMux”这个逻辑的一个实现。允许其中一个参数传入 nil 可能导致调用者以为他们可以给两个参数都传入 nil(Austin Luo:调用者可能想,既然第二个参数有默认实现,那第一个参数可能也有),但像这样调用:

http.Serve(nil, nil)

将导 panic 。

首选可变参数(var args)而非切片参数([]T)

强制调用者应该传递至少一个参数。我们可以像这样组合常规参数和可变参数:

func anyPositive(first int, rest ...int) bool {
	if first > 0 {
		return true
	}
	for _, v := range rest {
		if v > 0 {
			return true
		}
	}
	return false
}

相同类型相似含义参数的处理

func Max(a, b int) int

func CopyFile(to, from string) error

上述代码对于函数Max来说,参数的位置并不会影响到函数的结果,但是CopyFile就会对顺序有要求,那么这种情况下怎么写出可读性更高的代码呢?废话不多说,上代码:

type Source string

func (src Source) CopyTo(dest string) error {

return CopyFile(dest, string(src))

}

func main() {

var from Source = "presentation.md"

from.CopyTo("/tmp/backup")

}

这样 CopyFile 就总是可以被正确地调用——这也可以通过单元测试确定,也可以被设置为私有,进一步降低了误用的可能性。

变量声明规范

避免给变量加上与类型相关的任何后缀

比如:

type Config struct {
  //
}

func WriteConfig(w io.Writer, config *Config)

Config参数命名为config是多余的,我们知道它是个Config,函数签名上写得很清楚。

在这种情况建议考虑conf或者c——如果生命周期足够短的话。

如果在一个范围内有超过一个*Config,那命名为conf1、conf2的描述性就比original、updated更差,而且后者比前者更不容易出错。

变量声明方式

1.只声明,不初始化时,使用var。避免将包命名为base、common、util;

针对优化utils或helpers这种包名,我的建议是分析它们是在哪里被使用,并且是否有可能把相关函数挪到调用者所在的包。即便这可能导致一些重复的帮助类代码,但这也比在两个包之间引入一个导入依赖来的更好

2.既声明,也显式地初始化时,使用:=。
特定类型除外

length := uint32(0x80)

包的设计原则

包名

以包“提供”的东西来命名,而不是以“包含”的东西来命名

降低包之间的耦合度

每个 Go Package 事实上自身都是一个小的 Go 程序。
模块不向外部透露任何不必要的信息,也不依赖外部模块的实现。
一个好的 Go Package应该致力于较低的源码级耦合,这样,随着项目的增长,对一个包的更改不会级联影响其他代码库。那些“世界末日”似的重构让代码的更新优化变得极其困难,也让工作在这样的代码库上的开发者的生产效率极度地受限。

避免包级别的状态

编写可维护的程序的一个关键方面是松耦合——更改一个包,应该把对没有直接依赖它的包的影响降到最低。
在 Go 中有两种很好的方法可以实现松散耦合:

1.使用接口来描述函数或方法所需的行为。

2.避免使用全局状态
全局变量对每个函数都是可见的,但开发者可能意识不到全局变量的存在(即隐匿的参数),即使意识到并使用了全局变量,也可能意识不到该变量可能在别处被修改,导致全局变量的使用不可靠,依赖该变量状态(值)的函数被打破。

如果您想减少全局变量带来的耦合,那么:

将相关变量作为字段移动到需要它们的结构上。
使用接口来减少行为与该行为的实现之间的耦合

偏向更少、更大的包

一个 Java 包等效于一个独立的 .go 源文件;一个 Go 包等效于整个 Maven 模块或 .NET 程序集

从单一的 .go 文件开始,并且使用与包相同的名字。比如包http的第一个文件应该是 http.go,并且放到名为 http 的文件夹中。

随着包的逐渐增长,您可以根据职责拆分不同的部分到不同的文件。例如,将 Request 和 Response 类型拆分到 message.go 中,将 Client 类型拆分到 client.go 中,将 Server 类型拆分到 server.go 中。

如果您发现您的文件具有很相似的import声明时,考虑合并它们,否则确定两者的具体差异并优化重构它们。

不同的文件应该负责包的不同区域。messages.go 可能负责网络相关的 HTTP 请求和响应编组,http.go 可能包含低级网络处理逻辑,client.go 和 server.go 实现 HTTP 请求创建或路由的业务逻辑,等等。

避免复杂的包层次结构,克制分类的渴望

对于 Go 工具集来讲,Go 包的层次结构是没有意义的。例如,net/http 并不是 net 的子或子包。

如果您创建了不包含任何 .go 文件的中间目录,则不适用此建议。

使用internal包收敛公开API表面

把代码放置于名为 internal/ 的目录或子目录即可。 go 命令发现导入的包中包含 internal 路径,它就会校验执行导入的包是否位于以 internal 的父目录为根的目录树中。

例如,包 …/a/b/c/internal/d/e/f 只能被根目录树 …/a/b/c 中的代码导入,不能被 …/a/b/g 或者其他任何库中的代码导入。

确保 main 包越小越好

main 应当解析标识,打开数据库连接,初始化日志模块等等,然后将具体的执行交给其他高级对象。

快速返回,而不是深层嵌套

条件代码块在进入函数时立即断言前置条件

让零值变得有意义

假设没有明确提供显示初始化器,每个变量声明之后都会被自动初始化为零内存对应的值,这就是零值。零值与其类型有关:数值类型为0,指针为nil,切片、映射、管道等也同样(为nil)。

免责声明:
一切资料仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。资料来自网络,版权争议与本人无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我联系处理。

全网视频教程加微信a518958666获取
基于SpringCloud 微服务架构 广告系统设计与实现
系统学习docker
docker前后端分离实战
Docker+Kubernetes(k8s)微服务容器化实战
Go语言实战抽奖系统
Go语言开发分布式任务调度 轻松搞定高性能Crontab
20小时快速入门go语言
Java从零到企业级电商项目实战
SSM主流框架入门与综合项目实战
Socket网络编程进阶与实战

  1. 基于Python玩转人工智能最火框架 TensorFlow应用实践
  2. webapp书城开发
  3. 组件方式开发 Web App全站
  4. 前端到后台ThinkPHP开发整站
  5. MySQL性能管理及架构设计
  6. 响应式开发一招致胜
  7. 掌握React Native技术转型随意切换
  8. Yii 2.0开发一个仿京东商城平台
  9. Python高效编程技巧实战
  10. 快速开发轻量级App
  11. 6小时 jQuery开发一个前端应用
  12. Android架构师之路 网络层架构设计与实战
  13. 程序猿的内功修炼,学好算法与数据结构
  14. Vue.js高仿饿了么外卖App 1.0到2.0版本完美升级
  15. Android 专项测试 Python篇
  16. 微信小程序入门与实战 常用组件API开发技巧项目实战
  17. Android 5.0+高级动画开发 矢量图动画 轨迹动画路径变换
  18. Android自动化测试实战 Java篇 主流工具 框架 脚本
  19. Python升级3.6 强力Django+杀手级Xadmin打造在线教育平台
  20. 高性能可扩展MySQL数据库设计及架构优化 电商项目
  21. 带领新手快速开发Android App
  22. Angular JS 仿拉勾网 WebApp 开发移动端单页应用
  23. 组件化封装思想实战Android App
  24. React.js入门基础与案例开发
  25. Yii 2.0进阶版 高级组件 优化京东平台
  26. 双平台真实开发GitHub App React Native技术全面掌握
  27. 玩转算法面试 leetcode题库分门别类详细解析
  28. Thinkphp 5.0 仿百度糯米开发多商家电商平台
  29. ThinkPHP5.0正式版第二季:实战开发企业站【完结】
  30. 最容易上手的Vue 2.0入门实战教程
  31. 聚焦Python分布式爬虫必学框架Scrapy 打造搜索引擎
  32. Angular 4.0从入门到实战 打造股票管理网站
  33. Java Spring带前后端开发完整电商平台
  34. Node.js项目线上服务器部署与发布
  35. Java大牛 带你从0到上线开发企业级电商项目
  36. ThinkPHP 5.0开发微信小程序商场打通全栈项目架构
  37. ES6零基础教学 解析彩票项目
  38. React高级实战 打造大众点评 WebApp
  39. BAT大咖助力 全面升级Android面试
  40. 全程MVP手把手 打造IM即时通讯Android APP
  41. 微信服务号+Yii 2.0构建商城系统全栈应用
  42. 机器学习入门 Scikit-learn实现经典小案例
  43. 腾讯大牛亲授 Web 前后端漏洞分析与防御技巧
  44. IT段子手详解MyBatis遇到Spring 秒学Java SSM开发大众点评 难度中级
  45. Vue 2.0 高级实战-开发移动端音乐 WebApp
  46. 全新升级 Kotlin系统入门与进阶
  47. 对接真实数据 从0开发前后端分离企业级上线项目
  48. Android应用发展趋势必备武器 热修复与插件化
  49. Laravel 快速开发简书
  50. 以慕课网日志分析为例 进入大数据 Spark SQL 的世界
  51. Get全栈技能点 Vue2.0/Node.js/MongoDB 打造商城系统
  52. Python操作三大主流数据库
  53. 前端JavaScript面试技巧
  54. Java SSM快速开发仿慕课网在线教育平台
  55. Android通用框架设计与完整电商APP开发
  56. Spring Boot企业微信点餐系统
  57. 开发微信全家桶项目 Vue/Node/MongoDB高级技术栈全覆盖
  58. Web自动化测试 Selenium基础到企业应用
  59. 高性能的 PHP API 接口开发
  60. 企业级刚需Nginx入门,全面掌握Nginx配置+快速搭建高可用架构
  61. Angular 打造企业级协作平台
  62. Python Flask 构建微电影视频网站
  63. Spring Boot带前后端 渐进式开发企业级博客系统
  64. 从零开发Android视频点播APP
  65. 前端跳槽面试必备技巧
  66. 10小时入门大数据
  67. 让你页面速度飞起来 Web前端性能优化
  68. Google面试官亲授 升级Java面试
  69. LoadRunner 工具使用 企业级性能测试实战
  70. 360大牛带你横扫PHP职场 全面解读PHP面试
  71. Python前后端分离开发Vue+Django REST framework实战
  72. Spring Security技术栈开发企业级认证与授权
  73. PHP开发高可用高安全App后端
  74. 看得见的算法 7个经典应用诠释算法精髓
  75. 全网最热Python3入门+进阶 更快上手实际开发
  76. Android互动直播APP开发
  77. JMeter 深入进阶性能测试体系 各领域企业实战
  78. Node.js入门到企业Web开发中的应用
  79. SSM到Spring Boot 从零开发校园商铺平台
  80. 深度学习之神经网络核心原理与算法
  81. BAT大厂APP架构演进实践与优化之路
  82. PHP秒杀系统 高并发高性能的极致挑战
  83. Java开发企业级权限管理系统
  84. Redux+React Router+Node.js全栈开发
  85. Redis从入门到高可用,分布式实践
  86. ES6+ 开发电商网站的账号体系 JS SDK
  87. Spark Streaming实时流处理项目实战
  88. 快速上手Linux 玩转典型应用
  89. Python接口测试框架实战与自动化进阶
  90. Python3数据科学入门与实战
  91. Android高级面试 10大开源框架源码解析
  92. 移动端App UI 设计入门与实战
  93. 精通高级RxJava 2响应式编程思想
  94. Java企业级电商项目架构演进之路 Tomcat集群与Redis分布式
  95. Webpack + React全栈工程架构项目实战精讲
  96. 快速上手Ionic3 多平台开发企业级问答社区
  97. 全面系统讲解CSS 工作应用+面试一步搞定
  98. 跨平台混编框架 MUI 仿豆瓣电影 APP
  99. Kotlin打造完整电商APP 模块化+MVP+主流框架
  100. BAT大牛亲授 基于ElasticSearch的搜房网实战
  101. Python3入门机器学习 经典算法与应用
  102. Java秒杀系统方案优化 高性能高并发实战
  103. 四大维度解锁 Webpack3.0 工具全技能
  104. 手工测试企业项目实践及面试提升
  105. 基于Storm构建实时热力分布项目实战
  106. Java深入微服务原理改造房产销售平台
  107. 全网稀缺Python自动化运维项目实战
  108. 前端成长必经之路-组件化思维与技巧
  109. 基于Python玩转人工智能最火框架 TensorFlow应用实践
  110. Koa2框架从0开始构建预告片网站
  111. React16+React-Router4 从零打造企业级电商后台管理系统
  112. Google资深工程师深度讲解Go语言
  113. 微信小游戏入门与实战 刷爆朋友圈
  114. Elastic Stack从入门到实践
  115. Python移动自动化测试面试
  116. Python3数据分析与挖掘建模实战
  117. Tomcat+Memcached/Redis集群 构建高可用解决方案
  118. 系统学习Docker 践行DevOps理念
  119. Spring Cloud微服务实战
  120. 揭秘一线互联网企业 前端JavaScript高级面试
  121. OpenCV+TensorFlow 入门人工智能图像处理
  122. 基于Golang协程实现流量统计系统
  123. 移动端自动化测试Appium 从入门到项目实战Python版
  124. UI动效设计从入门到实战 PC与移动界面设计必学
  125. Java并发编程与高并发解决方案
  126. Vue核心技术 Vue+Vue-Router+Vuex+SSR实战精讲
  127. 韩天峰力荐 Swoole入门到实战打造高性能赛事直播平台
  128. Docker+Kubernetes(k8s)微服务容器化实践
  129. Python Flask高级编程
  130. ZooKeeper分布式专题与Dubbo微服务入门
  131. App界面设计利器Sketch 精选案例合集
  132. Python高级编程和异步IO并发编程
  133. 新浪微博资深大牛全方位剖析 iOS 高级面试
  134. Vue2.5开发去哪儿网App 从零基础入门到实战项目
  135. 最全面的Java接口自动化测试实战
  136. HBase+SpringBoot实战分布式文件存储
  137. Gradle3.0自动化项目构建技术精讲+实战
  138. 玩转数据结构 从入门到进阶
  139. MyCAT+MySQL 搭建高可用企业级数据库集群
  140. 验证码图像识别,快速掌握TensorFlow模型构建与开发
  141. SpringBoot2.0不容错过的新特性 WebFlux响应式编程
  142. 响应式开发一招致胜
  143. jquery源码分析
  144. AngularJS全栈开发知乎
  145. 揭秘一线互联网企业 前端JavaScript高级面试
  146. JavaScript版 数据结构与算法
  147. Koa2 实现电影微信公众号前后端开发
  148. Koa2+Nodejs+MongoDb 入门实战视频教程
  149. Node.js 从零开发 web server博客项目 前端晋升全栈工程师必备
  150. Vue.js 源码全方位深入解析
  151. Vue核心技术 Vue+Vue-Router+Vuex+SSR实战精讲
  152. Vue全家桶+SSR+Koa2全栈开发美团网
  153. 飞速上手的跨平台App开发
  154. 前端JS基础面试技巧
  155. 前端跳槽面试必备技巧
  156. 让你页面速度飞起来 Web前端性能优化
  157. 微信小程序商城构建全栈应用
  158. 移动Web APP开发之实战美团外卖
  159. Thinkphp 5.0 仿百度糯米开发多商家电商平台
  160. ThinkPHP5.0正式版第二季:实战开发企业站
  161. ThinkPHP 5.0开发微信小程序商场打通全栈项目架构
  162. 前端到后台ThinkPHP开发整站
  163. PHP从基础语法到原生项目开发
  164. PHP高性能 高价值的PHP API
  165. 360大牛全面解读PHP面试
  166. PHP开发高可用高安全App后端
  167. PHP秒杀系统 高并发高性能的极致挑战
  168. Swoole入门到实战打造高性能赛事直播平台
  169. YII 2.0开发一个仿京东商城平台



更多课程请加微信a518958666获取

╭══════════════════════════════════════════╮ ║
 ║ 说明:教程版权归原作者所有,本人只是负责搜集整理,本人 ║
║ 不承担任何技术及版权问题。本人分享的任何教程仅提 ║
║ 供学习参考,不得用于商业用途,请在下载后在24小时 ║
║ 内删除。 ║
║ ║
║ 1.请遵守中华人民共和国相关法律、条例 ║
║ 2.本人提供的各类视频教程仅供研究学习,本人不承担观看 ║
║ 本教程后造成的一切后果 ║
║ 3.本人不保证提供的教程十分安全或是完全可用,请下载后 ║
║ 自行检查 ║
║ 4.本人提供的教程均为网上搜集,如果该程序涉及 ║
║ 或侵害到您的版权请立即写信通知我们。 ║
║ 5.如不同意以上声明,请立即删除,不要使用,谢谢合作 ║
║ ║
╰═════════════════════════════════════════╯

你可能感兴趣的:(go,golang)