• GO.语言基础


    Go程序设计的一些规则
    Go之所以会那么简洁,是因为它有一些默认的行为:

    大写字母开头的变量是可导出的,也就是其它包可以读取的,是公用变量;小写字母开头的就是不可导出的,是私有变量

    大写字母开头的函数也是一样,相当于class中的带public关键词的公有函数;小写字母开头的就是有private关键词的私有函数。

    内建函数make、new
    make用于内建类型(map、slice 和channel)的内存分配。new用于各种类型的内存分配。

    内建函数new本质上说跟其它语言中的同名函数功能一样:new(T)分配了零值填充的T类型的内存空间,并且返回其地址,即一个*T类型的值。用Go的术语说,它返回了一个指

    针,指向新分配的类型T的零值。

    内建函数make(T, args)与new(T)有着不同的功能,make只能创建slice、map和channel,并且返回一个有初始值(非零)的T类型,而不是*T。本质来讲,导致这三个类型有所

    不同的原因是指向数据结构的引用在使用前必须被初始化。

    例如,一个slice,是一个包含指向数据(内部array)的指针、长度和容量的三项描述符;在这些项目被初始化之前,slice为nil。对于slice、map和channel来说,make初始

    化了内部的数据结构,填充适当的值。make返回初始化后的(非零)值。

    函数

    函数是Go里面的核心设计,它通过关键字func来声明,格式如:  ---支撑多个返回值,变参(不定数量的参数)

    func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
        //这里是处理逻辑代码
        //返回多个值
        return value1, value2
    }
    

    Go语言中有种不错的设计,即延迟defer语句,你可以在函数中添加多个defer语句。

    当函数执行到最后时,这些defer语句会按照逆序执行,最后该函数返回。

    特别是当你在进行一些打开资源的操作时,遇到错误需要提前返回,在返回前你需要关闭相应的资源,不然很容易造成资源泄露等问题。

    一般写打开一个资源有这样的操作:

    func ReadWrite() bool {
    	file.Open("file")
    	// do something
    	if failureX {
    		file.Close()
    		return false
    	}
    	if failureY {
    		file.Close()
    		return false
    	}
    	file.Close()
    	return true
    }
    

    我们看到上面有很多重复的代码,Go的defer有效解决了这个问题。使用它后,不但代码量减少了很多,而且程序变得更优雅。在defer后指定的函数会在函数退出前调用

    如果有很多调用defer,那么defer是采用后进先出模式

    func ReadWrite() bool {
    	file.Open("file")
    	defer file.Close()
    
    	if failureX {
    		return false
    	}
    	if failureY {
    		return false
    	}
    	return true
    }
    

    函数作为值、类型

    在Go中函数也是一种变量,我们可以通过type来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型。

    函数作为类型到底有什么好处呢?那就是可以把这个类型的函数当做值来传递

    package main
    
    import "fmt"
    
    type testInt func(int) bool // 声明了一个函数类型
    func isOdd(integer int) bool {
    	if integer%2 == 0 {
    		return false
    	}
    	return true
    }
    func isEven(integer int) bool {
    	if integer%2 == 0 {
    		return true
    	}
    	return false
    }
    
    // 声明的函数类型在这个地方当做了一个参数
    func filter(slice []int, f testInt) []int {
    	var result []int
    	for _, value := range slice {
    		if f(value) {
    			result = append(result, value)
    		}
    	}
    	return result
    }
    func main() {
    	slice := []int{1, 2, 3, 4, 5, 7}
    	fmt.Println("slice = ", slice)
    	odd := filter(slice, isOdd) // 函数当做值来传递了
    	fmt.Println("Odd elements of slice are: ", odd)
    	even := filter(slice, isEven) // 函数当做值来传递了
    	fmt.Println("Even elements of slice are: ", even)
    }
    

    函数当做值和类型在我们写一些通用接口的时候非常有用,觉得可以对比java等面向对象语言中的接口或者策略模式。

    Panic和Recover

    Go没有像Java那样的异常机制,它不能抛出异常,而是使用了panic和recover机制。一定要记住,你应当把它作为最后的手段来使用,也就是说,你的代码中应当没有,或者很少有panic的东西。

    Panic是一个内建函数,可以中断原有的控制流程,进入一个恐慌的流程中。当函数F调用panic,函数F的执行被中断,但是F中的延迟函数会正常执行,然后F返回到调用它的地方。在调用的地方,F的行为就像调用了panic。这一过程继续向上,直到发生panic的goroutine中所有调用的函数返回,此时程序退出。恐慌可以直接调用panic产生。也可以由运行时错误产生,例如访问越界的数组。

    Recover是一个内建的函数,可以让进入恐慌的流程中的goroutine恢复过来。recover仅在延迟函数中有效。在正常的执行过程中,调用recover会返回nil,并且没有其它任何效果。如果当前的goroutine陷入恐慌,调用recover可以捕获到panic的输入值,并且恢复正常的执行。

    panic使用 :

    var user = os.Getenv("USER")
    
    func init() {
    	if user == "" {
    		panic("no value for $USER")
    	}
    }
    

     下面这个函数检查作为其参数的函数在执行时是否会产生panic

    func throwsPanic(f func()) (b bool) {
    	defer func() {
    		if x := recover(); x != nil {
    			b = true
    		}
    	}()
    
    	f() //执行函数f,如果f中出现了panic,那么就可以恢复回来
    	return
    }
    

    main函数 和 init函数

    Go里面有两个保留的函数:init函数(能够应用于所有的package)和main函数(只能应用于package main)。这两个函数在定义时不能有任何的参数和返回值。虽然一个package里面可以写任意多个init函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个package中每个文件只写一个init函数。

    Go程序会自动调用init()和main(),所以你不需要在任何地方调用这两个函数。每个package中的init函数都是可选的,但package main就必须包含一个main函数。

    程序的初始化和执行都起始于main包。如果main包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到fmt包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包
    中的包级常量和变量进行初始化,接着执行init函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,然后执行main包中的init函数(如果存在的话),最后执行main函数。

    import导入包文件

    import(
         "fmt"
    )

    上面这个fmt是Go语言的标准库,其实是去goroot下去加载该模块,当然Go的import还支持如下两种方式来加载自己写的模块:

    1. 相对路径
    import  “./model”   //当前文件同一目录的model目录,但是不建议这种方式来import

    2. 绝对路径
    import  “shorturl/model”   //加载gopath/src/shorturl/model模块

    还有一些特殊的import方式:

    1. 点操作
    import(
    . "fmt"
    )
    这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名,也就是前面你调用的fmt.Println("hello world")可以省略的写成Println("hello world")

    2. 别名操作
    import(
    f  "fmt"
    )
    别名操作的话调用包函数时前缀变成了我们的前缀,即 f.Println("hello world")

    3. _操作
    import (
    "database/sql"
    _ "github.com/ziutek/mymysql/godrv"
    )
    _操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数。

    struct类型

    和其他语言一样,我们可以使用struct声明新的类型

    type person struct {
    	name string
    	age  int
    }
    

     Go支持只提供类型,而不写字段名的方式,也就是匿名字段,也称为嵌入字段。

    package main
    
    import "fmt"
    
    type Human struct {
    	name   string
    	age    int
    	weight int
    }
    type Student struct {
    	speciality string
    	Human      // 匿名字段,默认Student包含了Human所有字段
    }
    
    func main() {
    	mark := Student{Human{"Mark", 25, 120}, "Computer Science"}
    
    	fmt.Println("His speciality is ", mark.speciality)
    	fmt.Println("His name is ", mark.name)
    
    	mark.speciality = "AI"
    	fmt.Println("His speciality is ", mark.speciality)
    
    	mark.age = 46
    	fmt.Println("His age is", mark.age)
    }
    

    这里有一个问题:如果human里面有一个字段叫做phone,而student也有一个字段叫做phone,那么该怎么办呢?

    Go里面很简单的解决了这个问题,最外层的优先访问,也就是当你通过student.phone访问的时候,是访问student里面的字段,而不是human里面的字段。

    这样就允许我们去通过匿名字段继承的一些字段,当然如果我们想访问重载后对应匿名类型里面的字段,可以通过匿名字段名来访问。

    面向对象

    函数的另一种形态,带有接收者的函数,称为method。

    package main
    
    import "fmt"
    
    type Rectangle struct {
    	width, height float64
    }
    
    func area(r Rectangle) float64 {
    	return r.width * r.height
    }
    func main() {
    	r1 := Rectangle{12, 2}
    	r2 := Rectangle{9, 4}
    	fmt.Println("Area of r1 is: ", area(r1))
    	fmt.Println("Area of r2 is: ", area(r2))
    }
    

    使用method的时候重要注意几点

    1.虽然method的名字一模一样,但是如果接收者不一样,那么method就不一样

    2.method里面可以访问接收者的字段

    3.调用method通过.访问,就像struct里面访问字段一样

    值得说明的一点是,

    Receiver还可以是指针, 两者的差别在于, 指针作为Receiver会对实例对象的内容发生操作,而普通类型作为Receiver仅仅是以副本作为操作对象,并不对原实例对象发生操作。

    另外,method可以定义在任何你自定义的类型、内置类型、struct等各种类型上面。

    例如,给自定义类型定义method

    package main
    
    import "fmt"
    
    const (
    	WHITE = iota
    	BLACK
    	BLUE
    	RED
    	YELLOW
    )
    
    type Color byte
    
    type Box struct {
    	width, height, depth float64
    	color                Color
    }
    
    type BoxList []Box 
    
    func (b Box) Volume() float64 { //调用者不会被修改
    	return b.width * b.height * b.depth
    }
    
    func (b *Box) SetColor(c Color) { //调用者会被修改
    	b.color = c
    }
    
    func (bl BoxList) BiggestsColor() Color {
    	v := 0.00
    	k := Color(WHITE)
    	for _, b := range bl {
    		if b.Volume() > v {
    			v = b.Volume()
    			k = b.color
    		}
    	}
    	return k
    }
    
    func (bl BoxList) PaintItBlack() {
    	for i, _ := range bl {
    		bl[i].SetColor(BLACK)
    	}
    }
    func (c Color) String() string {
    	strings := []string{"WHITE", "BLACK", "BLUE", "RED", "YELLOW"}
    	return strings[c]
    }
    func main() {
    	boxes := BoxList{
    		Box{4, 4, 4, RED},
    		Box{10, 10, 1, YELLOW},
    		Box{1, 1, 20, BLACK},
    		Box{10, 10, 1, BLUE},
    		Box{10, 30, 1, WHITE},
    		Box{20, 20, 20, YELLOW},
    	}
    	fmt.Printf("We have %d boxes in our set
    ", len(boxes))
    	fmt.Println("The volume of the first one is", boxes[0].Volume(), "cm³")
    	fmt.Println("The color of the last one is", boxes[len(boxes)-1].color.String())
    	fmt.Println("The biggest one is", boxes.BiggestsColor().String())
    	fmt.Println("Let's paint them all black")
    	boxes.PaintItBlack()
    	fmt.Println("The color of the second one is", boxes[1].color.String())
    	fmt.Println("Obviously, now, the biggest one is", boxes.BiggestsColor().String())
    }
    

    Color作为byte的别名,上面SetColor这个method,它的receiver是一个指向Box的指针(定义SetColor的真正目的是想改变这个Box的颜色,如果不传Box的指针,那么

    SetColor接受的其实是Box的一个copy,也就是说method内对于颜色值的修改,其实只作用于Box的copy,而不是真正的Box。所以我们需要传入指针。)

    如果一个method的receiver是*T,你可以在一个T类型的实例变量V上面调用这个method,而不需要&V去调用这个method,反之如果一个method的receiver是T,你也可以在一个*T类型的变量P上面调用这个method,而不需要 *P去调用这个method

    method继承与重写

    类似于字段的继承,如果匿名字段实现了一个method,那么包含这个匿名字段的struct也能调用该method。

    同样,与匿名字段冲突一样的道理,可以在包含匿名字段的struct上定义一个同样method,重写了匿名字段的方法

    package main
    
    import "fmt"
    
    type Human struct {
    	name  string
    	age   int
    	phone string
    }
    type Student struct {
    	Human  //匿名字段
    	school string
    }
    type Employee struct {
    	Human   //匿名字段
    	company string
    }
    
    func (h *Human) SayHi() {
    	fmt.Printf("Hi, I am %s you can call me on %s
    ", h.name, h.phone)
    }
    
    //Employee重写Human的method
    func (e *Employee) SayHi() {
    	fmt.Printf("Hi, I am %s, I work at %s. Call me on %s
    ", e.name,
    		e.company, e.phone) //Yes you can split into 2 lines here.
    }
    func main() {
    	mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
    	sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
    	mark.SayHi()
    	sam.SayHi()
    }
    

    interface

    简单的说,interface是一组method的组合,我们通过interface来定义对象的一组行为。如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口。

    如果我们定义了一个interface的变量,那么这个变量里面可以存实现这个interface的任意类型的对象。

    package main
    
    import "fmt"
    
    type Human struct {
    	name  string
    	age   int
    	phone string
    }
    
    type Student struct {
    	school string
    	loan   float32
    	Human  //匿名字段
    }
    
    type Employee struct {
    	company string
    	money   float32
    	Human   //匿名字段
    }
    
    //Human实现Sayhi方法
    func (h Human) SayHi() {
    	fmt.Printf("Hi, I am %s you can call me on %s
    ", h.name, h.phone)
    }
    
    //Human实现Sing方法
    func (h Human) Sing(lyrics string) {
    	fmt.Println("La la la la...", lyrics)
    }
    
    //Employee重载Human的SayHi方法
    func (e Employee) SayHi() {
    	fmt.Printf("Hi, I am %s, I work at %s. Call me on %s
    ", e.name,
    		e.company, e.phone) //Yes you can split into 2 lines here.
    }
    
    // Interface Men被Human,Student和Employee实现
    type Men interface {
    	SayHi()
    	Sing(lyrics string)
    }
    
    func main() {
    	mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00}
    	paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100}
    	sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000}
    
    	var i Men
    
    	i = mike
    	i.SayHi()
    
    	i = sam
    	i.SayHi()
    
    	x := make([]Men, 3)
    	x[0], x[1], x[2] = paul, sam, mike
    	for _, value := range x {
    		value.SayHi()
    	}
    }
    

    由上面的代码可以知道,interface可以被任意的对象实现。一个对象也可以实现任意多个interface,

    其实,任意的类型都实现了空interface(我们这样定义:interface{}),也就是包含0个method的interface,这点类似于java里的object。

    如上面代码,如果我们定义了一个interface的变量,那么这个变量里面可以存实现这个interface的任意类型的对象。

    interface函数参数

    interface的变量可以持有任意实现该interface类型的对象,同样,也可以通过定义interface参数,让函数接受各种类型的参数。

    interface变量存储的类型

    如何反向知道interface变量中实际保存了的是哪个类型的对象。

    Comma-ok断言

    Go语言里面有一个语法: value, ok = element.(T),这里value就是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型。

    如果element里面确实存储了T类型的数值,那么ok返回true,否则返回false。

    另一种语法 element.(type)只能在switch中使用,如果你要在switch外面判断一个类型就使用comma-ok。

    package main
    
    import (
    	"fmt"
    	"strconv"
    )
    
    type Element interface{}
    
    type List []Element
    
    type Person struct {
    	name string
    	age  int
    }
    
    //定义了String方法,实现了fmt.Stringer
    func (p Person) String() string {
    	return "(name: " + p.name + " - age: " + strconv.Itoa(p.age) + " years)"
    }
    
    func main() {
    	list := make(List, 3)
    	list[0] = 1
    	list[1] = "Hello"
    	list[2] = Person{"Dennis", 70}
    
    	for index, element := range list {
    		if value, ok := element.(int); ok {
    			fmt.Printf("list[%d] is an int and its value is %d
    ", index, value)
    		} else if value, ok := element.(string); ok {
    			fmt.Printf("list[%d] is a string and its value is %s
    ", index, value)
    		} else if value, ok := element.(Person); ok {
    			fmt.Printf("list[%d] is a Person and its value is %s
    ", index, value)
    		} else {
    			fmt.Println("list[%d] is of a different type", index)
    		}
    	}
    }
    

     嵌入interface

    如果一个interface1作为interface2的一个嵌入字段,那么interface2隐式的包含了interface1里面的method。

    源码包container/heap里面有这样的一个定义:

    type Interface interface {
    	sort.Interface      //嵌入字段sort.Interface
    	Push(x interface{}) //a Push method to push elements into the heap
    	Pop() interface{}   //a Pop elements that pops elements from the heap
    }
    

     另一个例子就是io包下面的 io.ReadWriter ,他包含了io包下面的Reader和Writer两个interface。

    // io.ReadWriter
    type ReadWriter interface {
    	Reader
    	Writer
    }
    

     反射

    Go语言实现了反射,所谓反射就是动态运行时的状态。我们一般用到的包是reflect包。使用reflect一般分成三步:

    1.要去反射是一个类型的值(都实现了空interface),首先需要把它转化成reflect对象(reflect.Type或者reflect.Value,根据不同的情况调用不同的函数):

    t := reflect.TypeOf(i) //得到类型的元数据,通过 t 我们能获取类型定义里面的所有元素

    v := reflect.ValueOf(i) //得到实际的值,通过 v 我们获取存储在里面的值,还可以去改变值

    2.转化为reflect对象之后我们就可以进行一些操作了,也就是将reflect对象转化成相应的值:

    tag := t.Elem().Field(0).Tag //获取定义在struct里面的标签

    name := v.Elem().Field(0).String() //获取存储在第一个字段里面的值

    获取反射值能返回相应的类型和数值

    var x float64 = 3.4

    v := reflect.ValueOf(x)

    fmt.Println("type:", v.Type())

    fmt.Println("kind is float64:", v.Kind() == reflect.Float64)

    fmt.Println("value:", v.Float())

    最后,反射的话,那么反射的字段必须是可修改的,前面说过传值和传引用的区别,反射的字段必须是可读写的意思是,

    如果下面这样写,那么会发生错误

    var x float64 = 3.4

    v := reflect.ValueOf(x)

    v.SetFloat(7.1)

    如果要修改相应的值,必须这样写

    var x float64 = 3.4

    p := reflect.ValueOf(&x)

    v := p.Elem()

    v.SetFloat(7.1)

    GO并发

    GO从语言层面就支持了并行

    goroutine

    goroutine是Go并行设计的核心。goroutine说到底其实就是线程,但是他比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine

    之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易

    用、更高效、更轻便。goroutine是通过Go的runtime管理的一个线程管理器。goroutine通过go关键字实现了,其实就是一个普通的函数:go hello(a, b, c)

    channels

    goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。

    Go提供了一个很好的通信机制channel。channel可以与Unix shell 中的双向管道做类比:可以通过它发送或者接收值。这些值只能是特定的类型:channel类型。定义一个

    channel时,也需要定义发送到channel的值的类型。

    注意,必须使用make 创建channel:

    ci := make(chan int)

    cs := make(chan string)

    cf := make(chan interface{})

    channel通过操作符<-来接收和发送数据

    ch <- v // 发送v到channel ch.

    v := <-ch // 从ch中接收数据,并赋值给v

    package main
    
    import (
    	"fmt"
    )
    
    func sum(a []int, c chan int) {
    	sum := 0
    	for _, v := range a {
    		sum += v
    	}
    	c <- sum // send sum to c
    }
    func main() {
    	a := []int{7, 2, 8, -9, 4, 0}
    
    	fmt.Println(len(a))
    	fmt.Println(a[:3])
    
    	c := make(chan int)
    	go sum(a[:len(a)/2], c)
    	go sum(a[len(a)/2:], c)
    	x, y := <-c, <-c // receive from c
    	fmt.Println(x, y, x+y)
    }
    

     默认情况下,channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得Goroutines同步变的更加的简单,而不需要显式的lock。

    Buffered Channels

    默认的是非缓存类型的channel,不过Go也允许指定channel的缓冲大小,很简单,就是channel可以存储多少元素。

    ch:= make(chan bool, 4),创建了可以存储4个元素的bool 型channel。在这个channel 中,前4个元素可以无阻塞的写入。当写入第5个元素时,代码将会阻塞。

    package main
    
    import (
    	"fmt"
    )
    
    func main() {
    	c := make(chan int, 2) //修改2为1就报错,修改2为3可以正常运行
    	c <- 1
    	c <- 2
    	fmt.Println(<-c)
    	fmt.Println(<-c)
    }
    

    Range和Close

    上面例子中,需要读取两次c,不是很方便,考虑到这一点,可以通过range,像操作slice或者map一样操作缓存类型的channel.

    package main
    
    import (
    	"fmt"
    )
    
    func fibonacci(n int, c chan int) {
    	x, y := 1, 1
    	for i := 0; i < n; i++ {
    		c <- x
    		x, y = y, x+y
    	}
    	close(c)
    }
    func main() {
    	c := make(chan int, 10)
    	go fibonacci(cap(c), c)
    	for i := range c {
    		fmt.Println(i)
    	}
    }
    

    Select

    如果多个channel,Go里面提供了一个关键字select,通过select可以监听channel上的数据流动。

    select默认是阻塞的,只有当监听的channel中有发送或接收可以进行时才会运行,当多个channel都准备好的时候,select是随机的选择一个执行的。

    package main
    
    import "fmt"
    
    func fibonacci(c, quit chan int) {
    	x, y := 1, 1
    	for {
    		select {
    		case c <- x:
    			x, y = y, x+y
    		case <-quit:
    			fmt.Println("quit")
    			return
    		}
    	}
    }
    
    func main() {
    	c := make(chan int)
    	quit := make(chan int)
    	go func() {
    		for i := 0; i < 10; i++ {
    			fmt.Println(<-c)
    		}
    		quit <- 0
    	}()
    	fibonacci(c, quit)
    }
    

    c和quit,随机选择一个准备好的执行,两个线程执行,func线程中,先将c打印完在给quit赋值。

    select其实就是类似switch的功能,在select里面还有default语法,default就是当监听的channel都没有准备好的时候,默认执行的(select不再阻塞等待channel)。

    超时

    有时候会出现goroutine阻塞的情况,可以利用select来设置超时,来避免整个的程序进入阻塞。

    package main
    
    import "time"
    
    func main() {
    	c := make(chan int)
    	o := make(chan bool)
    	go func() {
    		for {
    			select {
    			case v := <-c:
    				println(v)
    			case <-time.After(5 * time.Second):
    				println("timeout")
    				o <- true
    				break
    			}
    		}
    	}()
    	println(<-o)
    }
    

    runtime goroutine

    runtime包中有几个处理goroutine的函数:
    Goexit:退出当前执行的goroutine,但是defer函数还会继续调用
    Gosched:让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。
    NumCPU:返回 CPU 核数量
    NumGoroutine:返回正在执行和排队的任务总数
    GOMAXPROCS:用来设置可以运行的CPU核数

    #笔记内容来自 《Go Web编程》

  • 相关阅读:
    Maven的安装
    Mongodb 分库解决锁率过大问题
    IntelliJ IDEA集成开发Maven工程
    soapUI4.5模拟客户端测试webservice接口,含性能测试
    Mongodb性能监控
    Mongodb内存释放
    ActiveMq队列数据监控器1小时1个Swing小程序
    运输公司对用户计算运输费用C语言109页
    给出一百分制,要求输出成绩等级'A''B''C''D''E'
    有一函数,写程序 输入x的值,输出y相应的值
  • 原文地址:https://www.cnblogs.com/shanhm1991/p/7144356.html
Copyright © 2020-2023  润新知