7 天从 Java 工程师转型为 Go 工程师
为什么要舍弃 Java,投奔 Go?
我从 2014 年开始接触 Java,对 Java 这门语言有着一种母语情结。相比于 C,Java 拥有面向对象、支持跨平台、垃圾回收等优秀特性。且 Java 的强大而成熟的生态体系、丰富的工具类等使 Java 能够支持了无数大型的系统架构。所以被国内以阿里为代表的广大企业用作编程主语言。
那么既然 Java 这么厉害,为什么还需要学 go 呢?
我的回答是 :Go 比 Java 更加简单、好用。
我们来简单地看一段 Java 和 go 的代码,就拿实现并发来说
显而易见,在代码简洁度上 Go 可以说秒杀 Java。
当然,Go 语言除了简单、好用以外,也有其他的一些非常强大的特性。
包括:
1. 并发与协程
2. 基于消息传递的通信方式
3. 丰富实用的内置数据类型
4. 函数多返回值
5. defer 机制
6. 反射(reflect)
7. 高性能 HTTP Server
因为 Go 语言的这些优势,所以最近几年得到了很大的发展。很多新兴技术包括 docker、kubernetes 等云端技术以及区块链等都是使用 Go 语言进行开发的。从这个意义上来说,Go 语言是比较“潮”的语言。技多不压身,那么就让我们开启 Go 语言之旅吧。
Go quick start
环境安装
安装环境比较简单,这里就不赘述了,具体可参考:http://www.runoob.com/go/go-environment.html
GoPath 和 GoRoot
通过上面教程的环境安装,你可以顺利输出一个 hello world,那么这个过程中发生了什么呢?先来看代码
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
使用过 Java 的同学应该是对 package 很熟悉了,Go 里面 package 的概念和 Java 是类似的。要生成 Go 可执行程序,必须建立一个名为 main 的package,并且在该 package 中必须包含一个名为 main() 的函数。“fmt” 也是一个包,那么这个包是怎么找到的呢?这里就要引出 GoRoot 和 GoPath 了,GoPath 是 Go 的工作目录,GoRoot 是 Go 的安装目录。
使用 go env 命令可以查看 GoPath 和 GoRoot: GoPath 目录约定有三个子目录
- src:存放源代码。按照 Go 语言约定,go run,go install 等命令默认会在此路径下执行;
- pkg:存放编译时生成的中间文件( * .a );
- bin: 存放编译后生成的可执行文件 ( 在项目内执行 go install,会在 bin 目录下生成一个可执行文件)。
当我们要引用 GitHub 上的开源包时,比如使用 https://github.com/garyburd/redigo 这个包来用 golang 进行 redis 的操作。可以执行 go get https://github.com/garyburd/redigo 命令,将会在 GoPath 的 src 目录下生成一个 /github.com/garyburd/redigo 这样的目录结构。
即可在项目中像引用 fmt 包一样引用
package main import ( "fmt" "github.com/garyburd/redigo/redis" ) func main() {
c, err := redis.Dial("tcp", "127.0.0.1:6379")
if err != nil {
fmt.Println("Connect to redis error", err)
return
}
fmt.Println("redis connect succ")
defer c.Close()
}
编译和运行
我们先把随便找一个空目录,新建 test.go,贴上代码启动 redis ,执行 go run test.go,即可出现 “redis connect succ” , 程序执行成功。
Go 命令
Go 提供了很多命令,包括打包、格式化代码、文档生成、下载第三方包等等诸多功能,我们在命令行工具上执行 go --help ,如下:
这里重点介绍几个常用的命令
-
编译并执行,只能作用于命令源码文件,一般用于开发中快速测试。上文我们通过执行 go run test.go ,完成了程序的编译运行。
-
go build 编译代码包或者源码文件。如果带上代码包名,则表示编译指定的代码包;如果带上源码文件,则表示编译指定源码文件。
-
go get 下载第三方代码包并编译安装 ,需要注意的是,它会下载安装到 GOPATH 环境变量配置的第一个工作区中。上文我们在使用 Go 连接 redis 时,通过 go get https://github.com/garyburd/redigo 命令,将 redis 包下载到 GoPath 的 src 目录下并进行编译安装。然后在代码中进行 import “github.com/garyburd/redigo/redis” 这个包则完成了导入。
-
go install 这个命令用于编译安装,可以作用于 main 包和非 main 包,然后将编译后的生成的执行文件存放到工程的 bin 目录下,将生成的归档文件(即静态链接库)存放到工程的 pkg 目录下。使用方式类似于 go build,可以在某个代码包目录下直接使用,也可以指定代码包使用。
比如在我们刚刚执行 go get https://github.com/garyburd/redigo 进行了 redis 包的编译安装。假如我们不使用 go get,而是通过下载 zip 压缩包的形式,在 GoPath 的 src 目录下建立解压,形成 github.com/garyburd/redigo 的目录结构 ,然后执行 go install github.com/garyburd/redigo/redis ,我们会发现在 GoPath 的 pkg 目录下会生成一个 这样的目录 /darwin_amd64/github.com/garyburd/redigo,redigo 目录下会生成一个 redis.a 的文件。
.a文件是编译过程中生成的,每个package都会生成对应的.a文件,Go在编译的时候先判断package的源码是否有改动,如果没有的话,就不再重新编译.a文件,这样可以加快速度。
- go env 用于打印GO语言的环境信息,如 GOPATH 是工作区目录,GOROOT 是 GO 语言安装目录,GOBIN 是通过 go install 命令生成可执行文件的存放目录(默认是当前工作区的 bin 目录下),GOEXE 为生成可执行文件的后缀
-
go vet 代码静态检查工具,如 go vet test.go。
完成 Java 语法 到 Go 语法的迅速切换
申明与赋值
数据类型
和 Java 类似,Go 也有基本数据类型和引用数据类型。
基本数据类型
- 布尔型:Java 申明用 boolean, Go 用 bool。
- 整型:Go 里面有 int8 int16 int32 uint64 ,分别对应 Java 中的 byte short int long,同时 Go 里面还有专门 表示无符号数的 uint8 uint 16 uint32 uint64。
- 浮点型:Go 里面 有 float32 float64 分别对应 Java 的 float 和 double。
- 字符串类型:Go 和 Java 一样,都是 string。
- 派生类型:包括 指针类型(Pointer)、数组类型、结构化类型(struct)、Channel 类型、函数类型、切片类型、接口类型(interface)、Map 类型 等。
变量申明赋值
Go 语言变量申明赋值有三种方式
先声明、后赋值
例如 :
var name string
name = "diuge"
Go 语言中用 var 申明变量,Java 语言中是以 分号 “ ; ” 进行语句分割,所以行末会有分号,而 Go 中没有.
Java 中变量申明是类型在变量前面,例如 string name , 但 Go 中相反,是变量在类型前,例如上文 var name string。
申明并且赋值
var name = "diuge"
编译器会根据值自行判定变量类型
使用 " := " 申明并赋值
name := "diuge"
这里省略 var 关键字
条件、循环语句
条件语句
Go 语言中 if 语句的语法如下:
if 布尔表达式 {
/* 在布尔表达式为 true 时执行 */
}
需要注意的是,Go 中条件语句,if 没有括号,例如
if a < 20 {
/* 如果条件为 true 则执行以下语句 */
fmt.Printf("a 小于 20\\n" )
}
此外,if 还有另外一种形式,它包含一个 statement 可选语句部分,该组件在条件判断之前运行。它的语法是
if statement; condition {
}
例如:
if a := 10 ; a < 20 {
/* 如果条件为 true 则执行以下语句 */
fmt.Printf("a 小于 20\\n" )
}
循环语句
与 Java 不同的是,Go 语言只提供了 for 循环,没有 while 循环。
但是在 Go 中,可以使用 for 循环实现 Java while 循环的效果。
Go 语言的 For 循环有 3 种形式,只有其中的一种使用分号。
第一种 :和 Java 的 for 语法类似,语法格式如下 :
for init; condition; post {
}
init: 一般为赋值表达式,给控制变量赋初值;
condition: 关系表达式或逻辑表达式,循环控制条件;
post: 一般为赋值表达式,给控制变量增量或减量。
例如 : for i :=0 ; i < 10 ; i ++ {
fmt.Println("i = " + i)
}
这里是不是很熟悉? 跟 Java 唯一不一样的地方在于 Go 中 for 循环没有小括号。
第二种 :和 Java while 循环 类似,语法格式如下 :
for condition { }
例如 :
for 0 == 0 {}
for true {}
第三种 : 死循环,类似 while (true) {} , 语法格式如下:
for { }
异常处理
学 Java 的大家可能都知道,Java 中有 exception 的 try — catch — finally 异常捕获机制 ,Go 语言中一般不采用这种异常捕获机制,而是通过下面几种方式进行异常处理。
error 接口
Go 语言通过内置的错误接口提供了非常简单的错误处理机制。
error 类型是一个接口类型,这是它的定义:
type error interface {
Error() string
}
使用 errors.New () 可以返回一个 error 信息 例如 :
func checkParam(username string, password string) error {
if username == "" || password == "" {
return errors.New("params error")
}
}
一般来说, error 是 Go 语言中最常见的处理错误的方式,通过返回 error,处理 error, 产生类似 Java exception 的效果。
defer 语句
在 Go 语言中,可以使用关键字defer向函数注册退出调用,即主调函数退出时,defer后的函数才会被调用。
defer 语句的作用是不管程序是否出现异常,均在函数退出时自动执行相关代码。(相当于 Java 中的 finally )
例如 :
func main() {
for i := 0; i < 5; i++ {
defer fmt.Println(i)
}
}
其执行结果为 :
4
3
2
1
0
所以,我们在进行数据库连接、文件、锁 操作时 ,一般都会使用 defer 语句进行数据库连接的释放,释放文件句柄和锁的释放。
panic-recover 机制
学过 Java 的同学都知道,Java 中异常分为运行时异常和非运行时异常。我们在上文说到可以在方法中抛出 error 错误来实现异常的捕获, error 只能针对预期内的错误,因为你是预判这段程序可能出现 异常逻辑,才会去主动调用 errors.New () 生成一个 error 。但是对于一个方法来说,我们不可能预判到所有的异常情况,那假如某一个隐藏 bug 导致程序崩溃了怎么办呢?这里就需要引入 panic-recover 机制了。
假如代码运行时异常崩溃了,此时 Go 会自动 panic,Go 的每次 panic 都是非常消耗性能的,且 Go 是单线程,所以,我们应该尽量去避免 使用 panic。
panic () 是一个内建函数,可以中断原有的控制流程,进入一个令人 panic (恐慌 ,即 Java 中的异常)的流程中。当函数 F 调用 panic,函数 F的执行被中断,但是 F 中的延迟函数(必须是在 panic 之前的已加载的 defer )会正常执行,然后 F 返回到调用它的地方。在调用的地方,F 的行为就像调用了panic。这一过程继续向上,直到发生 panic 的 goroutine 中所有调用的函数返回,此时程序退出。异常可以直接调用 panic 产生。也可以由运行时错误产生,例如访问越界的数组。
recover () 是一个内建的函数,可以让进入令人恐慌的流程中的 goroutine 恢复过来。recover 仅在延迟函数中有效。在正常的执行过程中,调用 recover 会返回 nil ,并且没有其它任何效果。如果当前 goroutine 陷入 panic ,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。
一般情况下,recover () 应该在一个使用 defer 关键字的函数中执行以有效截取错误处理流程。如果没有在发生异常的 goroutine 中明确调用恢复过程(使用 recover 关键字),会导致该 goroutine 所属的进程打印异常信息后直接退出。
这里结合自定义的 error 类型给出一个使用 panic 和 recover 的完整例子:
package main import ( "fmt" ) //定义除法运算函数 func Devide(num1, num2 int) int {
if num2 == 0 {
panic("num cannot be 0")
} else {
return num1 / num2
}
}
func main() {
var a, b int
fmt.Scanf("%d %d", &a, &b)
defer func() {
if r := recover(); r != nil {
fmt.Printf("panic的内容%v\\n", r)
}
}()
rs := Devide(a, b)
fmt.Println("结果是:", rs)
}
Go 语言特性
这里指的特性是数据类型以及语法上的特性,不上升到综合层次
函数
在 Java 中,函数是只能有单一返回值的,但是在 Go 中,函数可以支持多返回值。
例如 :
func swap ( x string , y string ) ( string , string ){
return y , x
}
Go 指针
Java 是不支持指针类型的,但是学过 C 的同学可能对指针有或多或少的了解,Go 语言也是支持指针类型的。
我们都知道,变量是一种使用方便的占位符,用于引用计算机内存地址。Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。
package main
import "fmt"
func main() {
var a int = 10
fmt.Printf("变量的地址: %x\\n", &a )
}
上面程序的输出结果为:
变量的地址: 20818a220
指针声明格式如下:
var var_name *var-type
var-type 为指针类型,var_name 为指针变量名,* 号用于指定变量是作为一个指针。以下是有效的指针声明:
var ip *int /\* 指向整型\*/
var fp *float32 /\* 指向浮点型 \*/
在指针类型前面加上 * 号(前缀)即可获取指针所指向的内容。
package main
import "fmt"
func main() {
var a int= 20 /\* 声明实际变量 \*/
var ip *int /\* 声明指针变量 \*/
ip = &a /\* 指针变量的存储地址 \*/
fmt.Printf("a 变量的地址是: %x\\n", &a )
/\* 指针变量的存储地址 \*/
fmt.Printf("ip 变量储存的指针地址: %x\\n", ip )
/\* 使用指针访问值 \*/
fmt.Printf("\*ip 变量的值: %d\\n", *ip )
}
指针的操作比较复杂,这里也只是对指针进行了简单的介绍,定义和程序都来自第三方网站,所以大家想更多了解指针的话可以自行研究一下,这里不赘述。
**结构体 **
Go 语言中结构体类似 Java 中的类 ( class )**
例如, Java 中对一个 Person 类的定义如下 :
class Person {
string name;
int age;
}
在 Go 语言中, 用结构体 ( struct ) 表示如下 :
type Person struct {
name string
age int
}
**切片 **
Java 中的数组长度在申明时就已经固定了,但是 Go 提供了一种类似 “ 动态数组 ” 结构的数据类型,这种类型就是切片 slice。
slice 的本质是一个数据结构,实现了对数组操作的封装。
切片 slice 的申明语法如下 :
var identifier []type
你可以申明一个 int32 类型的 slice
var array []int32
你会发现 slice 和数组的申明语法是不同的。
Go 语言中申明数组时,是需要指定长度的, 比如 :
var array [10] int32
初始化操作也不一样,对 slice 的初始化是使用 make 初始化
array = make ( []int32 , 10 )
而对数组的初始化是
array = [10] int32 {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
对 slice 的操作是 Go 语言中最常用的操作之一,包括 :
- append : 实现对 slice 元素的添加 例如 :
array := make ( []int32 , 10 )
array = append ( array, 1 )
array = append ( array, 2 )
- 截取 : 可以通过设置下限及上限来设置截取切片 [lower-bound:upper-bound],实例如下:
package main
import "fmt"
func main() {
/\* 创建切片 \*/
numbers := []int{0,1,2,3,4,5,6,7,8}
/\* 打印原始切片 \*/
fmt.Println("numbers ==", numbers)
/\* 打印子切片从索引1(包含) 到索引4(不包含)\*/
fmt.Println("numbers[1:4] ==", numbers[1:4])
/\* 默认下限为 0\*/
fmt.Println("numbers[:3] ==", numbers[:3])
/\* 默认上限为 len(s)\*/
fmt.Println("numbers[4:] ==", numbers[4:])
}
func printSlice(x []int){
fmt.Printf("len=%d slice=%v \\n" , len(x) , x)
}
执行以上代码输出结果为:
numbers == [0 1 2 3 4 5 6 7 8]
numbers[1:4] == [1 2 3]
numbers[:3] == [0 1 2]
numbers[4:] == [4 5 6 7 8]
- 获取切片长度 : 通过 len 方法,可以获取 slice 长度
len ( slice )
- 遍历 : 通过 range 关键字进行遍历,range 遍历是 Go 中特有的一种遍历方式,除了可以对 slice 进行遍历,还可以对 数组、map、string 字符串 等进行遍历。例如 :
package main
import "fmt"
func main() {
//这是我们使用range去求一个slice的和。使用数组跟这个很类似
nums := []int{2, 3, 4}
sum := 0
for _, num := range nums {
sum += num
}
fmt.Println("sum:", sum)
//在数组上使用range将传入index和值两个变量。上面那个例子我们不需要使用该元素的序号,所以我们使用空白符"\_"省略了。有时侯我们确实需要知道它的索引。
for i, num := range nums {
if num == 3 {
fmt.Println("index:", i)
}
}
//range也可以用在map的键值对上。
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -\> %s\\n", k, v)
}
//range也可以用来枚举Unicode字符串。第一个参数是字符的索引,第二个是字符(Unicode的值)本身。
for i, c := range "go" {
fmt.Println(i, c)
}
}
**并发 **
之前说到了,相比于 Java ,Go 是天然支持并发的。
在 go 语言中,每一个线程我们把它叫做 goroutine。
goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。
goroutine 语法格式:
go 函数名( 参数列表 ),例如:
go f (x, y, z)
表示新开一个线程 执行 f (x , y , z) 这个方法。
strong text 更多并发的知识点下文会继续介绍,这里不再赘述。
Java 面向对象特性在 Go 中是如何实现的
我们知道 Java 这门语言的流行跟 它面向对象的特性是分不开的。那么 Java 的封装、继承、多态 在 Go 中是如何实现的呢?
封装
Java 中的封装主要是通过访问权限控制实现的。
在 Go 语言中,并没有 public ,private 这些权限控制符。那么 go 是如何实现 结构体的封装的呢 ?
在 Go 语言中,是通过约定来实现权限控制的。变量和方法都遵守驼峰式命名。变量和方法的首字母大写,相当于 public,变量和方法的首字母小写,相当于 private。同一个包中访问,相当于 default ,由于 Go 语言没有继承,所以也没有 protected 权限。
上面刚说到,Go 语言是没有继承的。但是 Go 语言可以通过结构体之间的组合来实现类似 Java 中继承的效果。
假如把 Go 中 struct 看做 Java 中的类,在 struct 中可以包含其他的struct,继承内部 struct 的方法和变量,同时可以重写,代码如下:
package main
import "fmt"
type oo struct {
inner
ss1 string
ss2 int
ss3 bool
}
type inner struct {
ss4 string
}
func (i *inner) testMethod () {
fmt.Println("testMethod is called!!!")
}
func main() {
oo1 := new(oo)
fmt.Println("ss4无值:"+oo1.ss4)
oo1.ss4 = "abc"
fmt.Println("ss4已赋值"+oo1.ss4)
oo1.testMethod()//继承调用
oo1.inner.testMethod()//继承调用 这里也可以重写
}
多态
Java 中的多态是通过 extends class 或者 implements interface 实现的,在 Go 中既没有 extends,也没有 implements ,那么 Go 中是如何实现多态的呢 ?
我们来看以下代码,Girl 和 Boy 都实现了 Person 。 在 Go 语言中,只要某个 struct 实现了某个 interface 的所有方法,那么我们就认为这个 struct 实现了这个类(相当于 Java 中的implements)。
package main import ( "fmt" ) type Person interface {
Sing ()
}
type Girl struct {
Name string
}
type Boy struct {
Name string
}
func (this *Girl) Sing () {
fmt.Println("Hi, I am " + this.Name)
}
func (this *Boy) Sing () {
fmt.Println("Hi, I am " + this.Name)
}
func main() {
g := &Girl{"Lucy"}
b := &Boy{"Dave"}
p := map[int]Person{}
p[0] = g
p[1] = b
for _, v := range p {
v.Sing()
}
}
Go 并发
Java 中的并发是通过继承 Thread 类或者实现 Runnable 接口来实现的。我们前面说过,Go 并发通过 Go 关键字就可以实现,go f (x, y, z) 的形式即可以通过 新开 goroutine 实现 Go 并发。那么 goroutine 之间是如何进行通信与同步的呢
channel
channel 是 goroutine 之间通信的一种方式,可以类比成 Unix 中的进程的通信方式管道。
可以用 channel 操作符 <- 对其发送或者接收值。
ch <- v // 将 v 送入 channel ch。
v := <-ch // 从 ch 接收,并且赋值给 v。
(“箭头”就是数据流的方向。)
和 map 与 slice 一样,channel 使用前必须创建:
ch := make(chan int)
默认情况下,在另一端准备好之前,发送和接收都会阻塞。这使得 goroutine 可以在没有明确的锁或竞态变量的情况下进行同步。
例如 :
package main
import "fmt"
func sum(a []int, c chan int) {
sum := 0
for _, v := range a {
sum += v
}
c <- sum // 将和送入 c
}
func main() {
a := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(a[:len(a)/2], c) go sum(a[len(a)/2:], c) x, y := <-c, <-c // 从 c 中获取
fmt.Println(x, y, x+y)
}
channel 可以是带缓冲的。为 make 提供第二个参数作为缓冲长度来初始化一个缓冲 channel:
ch := make(chan int, 100)
向缓冲 channel 发送数据的时候,只有在缓冲区满的时候才会阻塞。当缓冲区清空的时候接受阻塞。
同时,可以使用 close© 来关闭一个 channel。
select
select 语句使得一个 goroutine 在多个通讯操作上等待。
select 会阻塞,直到条件分支中的某个可以继续执行,这时就会执行那个条件分支。当多个都准备好的时候,会随机选择一个。
当 select 中的其他条件分支都没有准备好的时候,default
分支会被执行。
例如 :
package main
import (
"fmt"
"time"
)
func main() {
tick := time.Tick(100 * time.Millisecond)
boom := time.After(500 * time.Millisecond)
for {
select {
case <-tick:
fmt.Println("tick.")
case <-boom:
fmt.Println("BOOM!")
return
default:
fmt.Println(" .")
time.Sleep(50 * time.Millisecond)
}
}
}
锁
这里只介绍一种互斥锁,其他类型的锁读者可以自行了解。
互斥锁用 var mutex sync.Mutex 申明 ,通过 mutex.Lock() 进行加锁,通过 mutex.Unlock() 进行解锁。
例如 :
func main() {
var common int = 100
var mutex sync.Mutex
for {
go func() {
mutex.Lock()
if common > 0 {
common --
fmt.Println(common)
}
mutex.Unlock()
}()
}
}
这里 common 是对所有线程都可见的共享内存,所以对common 的操作需要加锁。我们发现 Go 语言的互斥锁的使用是非常简单的,这也符合 Go 语言的设计理念。
Go RPC 调用
RPC 最常用的方式还是走 HTTP 协议,可以有 GET 和 POST 两种方式。
GET
这里贴上一个简单的发送 HTTP Get 请求的代码:
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
// 生成默认client
client := &http.Client{}
// 生成要访问的url
url := "http://www.baidu.com"
// 构造请求
request, err := http.NewRequest("GET", url, nil)
if err != nil {
fmt.Println("new request error : " + err.Error())
}
// 处理返回结果
response, err := client.Do(request)
// 处理错误
if err != nil {
fmt.Println("do request error : " + err.Error ())
}
body, err := ioutil.ReadAll ( resp.Body )
fmt.Println( string (body) )
}
POST
比较简单,直接贴代码 :
func request () {
client := &http.Client{}
req, err := http.NewRequest("POST", "www.baidu.com", strings.NewReader(""))
if err != nil {
// handle error
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Cookie", "uin=1812672342")
resp, err := client.Do(req)
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
// handle error
}
fmt.Println(string(body))
}
一个小 Demo:用 Go 语言实现抽奖系统
附送上一个小 demo ,使用 Go 写的,非常简单,本来奖品应该用 redis 存储,这里偷懒了一下,通过配置文件初始化了。
讲一下 抽奖算法是参考 微信平台的抽奖算法:https://www.xuanfengge.com/luck-draw.html(可以防止奖品被很快抽完,同时保证每个人的抽奖概率是相同的)。这里用 Go 重写了抽奖算法,如下:
func GetAward(awardBatches []AwardBatch) (AwardBatch, error) {
startTime , _ := ParseStringToTime(conf.Award.StartTime)
endTime , _ := ParseStringToTime(conf.Award.EndTime)
award , err := RandomGetAwardBatch(awardBatches)
if err != nil {
return AwardBatch{}, err
}
totalAmount := award.GetTotalAmount()
totalBalance := award.GetTotalBalance()
updateTime := award.GetUpdateTime()
detaTime := (endTime - startTime) / totalAmount
currentTime := time.Now().Unix()
r := rand.New(rand.NewSource(updateTime))
// 计算下一个奖品的释放时间
releaseTime := startTime + (totalAmount - totalBalance) * detaTime + int64(r.Int()) % detaTime
fmt.Println("releaseTime : " + fmt.Sprintf("%d", releaseTime) + " currentTime : " + fmt.Sprintf("%d",currentTime))
if (currentTime < releaseTime) {
return AwardBatch{} , errors.New(" currentTime not in award release period ")
}
return award, nil
}
func RandomGetAwardBatch(awardBatches []AwardBatch) ( AwardBatch , error ) {
if len(awardBatches) == 0 {
return AwardBatch{} , errors.New("empty param awardBatches")
}
weight := int64(0)
for _, awardBatch := range awardBatches {
weight += awardBatch.GetTotalBalance()
}
if weight == 0 {
return AwardBatch{}, errors.New("weight is 0")
}
r := rand.New(rand.NewSource(weight))
num := r.Int63n(weight)
for _, awardBatch := range awardBatches {
num -= awardBatch.GetTotalBalance()
if num < 0 {
return awardBatch , nil
}
}
return AwardBatch{}, errors.New("randomGetAwardBatch should shoot at least one batch")
}
我把它放在了 GitHub 上 https://github.com/diubrother/award,可以用来当作 Go 入门级的一个小 demo。
我的 Go 入门之路
为什么说 7 天转型呢?因为我当时就是用了 7 天左右。写 Go 的第一行代码时,很不适应,无论是变量的命名,还是没有行末分隔符,以及 Go 最让我不适应的一点是 数据类型只能显示转换,举个例子,如果不显示把 uint 32 转成 int64 或者 uint64 ,你的程序是永远编译不通过的。
个人觉得,学一门语言最快还是从使用开始,语法看个一两天,一定要对着用例 code ,对语法有个基本的印象就可以开始写需求或者功能了。可以尝试着写一些小的需求。我当时就是通过一个小的业务需求,直接对着别人的代码抄,然后抄个两三天,把你的代码编译、调试、测试、上线等整套流程走完,基本上就算是初步上手了。
这里推荐一个我当时入门看的两个网站,这篇 chat 里面几个例子也是引用了这两个网站的。
第一个是大家熟知的菜鸟教程 : http://www.runoob.com/go/go-tutorial.html (1-2 day 熟悉一下 go 语法)
第二个是 go 指南 :http://tour.studygolang.com/welcome/1 (这个相对于菜鸟教程的优势就是有比较多的例子)
好了,言尽于此,祝君好运。
欢迎关注我的公众号,回复关键字“大礼包” ,将会有大礼相送!!! 祝各位面试成功!!!