从别人的代码中学习golang系列--02
这篇博客还是整理从https://github.com/LyricTian/gin-admin 这个项目中学习的golang相关知识
作者在项目中使用了https://github.com/google/wire 做依赖注入,这个库我之前没有使用过,看了作者代码中的使用,至少刚开始是看着优点懵,不知道是做什么,所以这篇博客主要就是整理这个包的使用
依赖注入是什么?
如果你搜索依赖注入,百度百科里可能先看到的是控制反转,下面是百度百科的解释
控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。
这样的解释可能还是不好理解,所以我们通过一个简单的代码来理解应该就清楚很多。
我们用程序实现:小明对世界说:"hello golang"
这里将小明抽象为People 说的内容抽象为: Message 小明说 hello golang 抽象为:Event, 代码如下:
package main
import "fmt"
var msg = "Hello World!"
func NewMessage() Message {
   return Message(msg)
}
// 要说的内容的抽象
type Message string
func NewPeople(m Message) People {
   return People{name: "小明", message: m}
}
// 小明这个人的抽象
type People struct {
   name    string
   message Message
}
// 小明这个人会说话
func (p People) SayHello() string {
   msg := fmt.Sprintf("%s 对世界说:%s\n", p.name, p.message)
   return msg
}
func NewEvent(p People) Event {
   return Event{people: p}
}
// 小明去说话这个行为抽象为一个事件
type Event struct {
   people People
}
func (e Event) start() {
   msg := e.people.SayHello()
   fmt.Println(msg)
}
func main() {
   message := NewMessage()
   people := NewPeople(message)
   event := NewEvent(people)
   event.start()
}
从上面这个代码我们可以看出,我们必须先初始化一个NewMessage, 因为NewPeople 依赖它,NewEvent 依赖NewPeople. 这还是一种比较简单的依赖关系,实际生产的依赖关系可能会更复杂,那么什么好的办法来处理这种依赖,https://github.com/google/wire 就是来干这件事情的。
wire依赖注入例子
栗子1
安装: go get github.com/google/wire/cmd/wire
上面的代码,我们用wire的方式实现,代码如下:
package main
import (
   "fmt"
   "github.com/google/wire"
)
var msg = "Hello World!"
func NewMessage() Message {
   return Message(msg)
}
// 要说的内容的抽象
type Message string
func NewPeople(m Message) People {
   return People{name: "小明", message: m}
}
// 小明这个人的抽象
type People struct {
   name    string
   message Message
}
// 小明这个人会说话
func (p People) SayHello() string {
   msg := fmt.Sprintf("%s 对世界说:%s\n", p.name, p.message)
   return msg
}
func NewEvent(p People) Event {
   return Event{people: p}
}
// 小明去说话这个行为抽象为一个事件
type Event struct {
   people People
}
func (e Event) start() {
   msg := e.people.SayHello()
   fmt.Println(msg)
}
func InitializeEvent() Event {
   wire.Build(NewEvent, NewPeople, NewMessage)
   return Event{}
}
func main() {
   e := InitializeEvent()
   e.start()
}
这里我们不用再手动初始化NewEvent, NewPeople, NewMessage,而是通过需要初始化的函数传递给wire.Build , 这三者的依赖关系,wire 会帮我们处理,我们通过wire . 的方式生成代码:
➜  useWireBaseExample2 wire .
wire: awesomeProject/202006/useWireBaseExample2: wrote /home/fan/codes/go_project/awesomeProject/202006/useWireBaseExample2/wire_gen.go
➜  useWireBaseExample2
会在当前目录下生成wire_gen.go的代码,内容如下:
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
package main
import (
   "fmt"
)
// Injectors from main.go:
func InitializeEvent() Event {
   message := NewMessage()
   people := NewPeople(message)
   event := NewEvent(people)
   return event
}
// main.go:
var msg = "Hello World!"
func NewMessage() Message {
   return Message(msg)
}
// 要说的内容的抽象
type Message string
func NewPeople(m Message) People {
   return People{name: "小明", message: m}
}
// 小明这个人的抽象
type People struct {
   name    string
   message Message
}
// 小明这个人会说话
func (p People) SayHello() string {
   msg2 := fmt.Sprintf("%s 对世界说:%s\n", p.name, p.message)
   return msg2
}
func NewEvent(p People) Event {
   return Event{people: p}
}
// 小明去说话这个行为抽象为一个事件
type Event struct {
   people People
}
func (e Event) start() {
   msg2 := e.people.SayHello()
   fmt.Println(msg2)
}
func main() {
   e := InitializeEvent()
   e.start()
}
代码中wire为我们生成了如下代码:
// Injectors from main.go:
func InitializeEvent() Event {
   message := NewMessage()
   people := NewPeople(message)
   event := NewEvent(people)
   return event
}
在看看我们刚开始写的代码,发现其实是一样的,是不是感觉方便了很多。
注意:当使用 Wire 时,我们将同时提交 Wire.go 和 Wire _ gen 到代码仓库
wire 能做的事情很多,如果我们相互依赖的初始化其中有初始化失败的,wire也能帮我们很好的处理。
栗子2
package main
import (
   "errors"
   "fmt"
   "os"
   "time"
   "github.com/google/wire"
)
var msg = "Hello World!"
func NewMessage() Message {
   return Message(msg)
}
// 要说的内容的抽象
type Message string
func NewPeople(m Message) People {
   var grumpy bool
   if time.Now().Unix()%2 == 0 {
      grumpy = true
   }
   return People{name: "小明", message: m, grumpy: grumpy}
}
// 小明这个人的抽象
type People struct {
   name    string
   message Message
   grumpy  bool // 脾气是否暴躁
}
// 小明这个人会说话
func (p People) SayHello() string {
   if p.grumpy {
      // 脾气暴躁,心情不好
      msg := "Go away !"
      return msg
   }
   msg := fmt.Sprintf("%s 对世界说:%s\n", p.name, p.message)
   return msg
}
func NewEvent(p People) (Event, error) {
   if p.grumpy {
      return Event{}, errors.New("could not create event: event greeter is grumpy")
   }
   return Event{people: p}, nil
}
https://github.com/LyricTian/gin-admin
// 小明去说话这个行为抽象为一个事件
type Event struct {
   people People
}
func (e Event) start() {
   msg := e.people.SayHello()
   fmt.Println(msg)
}
func InitializeEvent() (Event, error) {
   wire.Build(NewEvent, NewPeople, NewMessage)
   return Event{}, nil
}
func main() {
   e, err := InitializeEvent()
   if err != nil {
      fmt.Printf("failed to create event: %s\n", err)
      os.Exit(2)
   }
   e.start()
}
更改之后的代码初始化NewEvent 可能就会因为People.grumpy 的值而失败,通过wire生成之后的代码
// Injectors from main.go:
func InitializeEvent() (Event, error) {
   message := NewMessage()
   people := NewPeople(message)
   event, err := NewEvent(people)
   if err != nil {
      return Event{}, err
   }
   return event, nil
}
栗子3
我们再将上面的代码进行更改:
package main
import (
   "errors"
   "fmt"
   "os"
   "time"
   "github.com/google/wire"
)
func NewMessage(msg string) Message {
   return Message(msg)
}
// 要说的内容的抽象
type Message string
func NewPeople(m Message) People {
   var grumpy bool
   if time.Now().Unix()%2 == 0 {
      grumpy = true
   }
   return People{name: "小明", message: m, grumpy: grumpy}
}
// 小明这个人的抽象
type People struct {
   name    string
   message Message
   grumpy  bool // 脾气是否暴躁
}
// 小明这个人会说话
func (p People) SayHello() string {
   if p.grumpy {
      // 脾气暴躁,心情不好
      msg := "Go away !"
      return msg
   }
   msg := fmt.Sprintf("%s 对世界说:%s\n", p.name, p.message)
   return msg
}
func NewEvent(p People) (Event, error) {
   if p.grumpy {
      return Event{}, errors.New("could not create event: event greeter is grumpy")
   }
   return Event{people: p}, nil
}
// 小明去说话这个行为抽象为一个事件
type Event struct {
   people People
}
func (e Event) start() {
   msg := e.people.SayHello()
   fmt.Println(msg)
}
func InitializeEvent(msg string) (Event, error) {
   wire.Build(NewEvent, NewPeople, NewMessage)
   return Event{}, nil
}
func main() {
   msg := "Hello Golang"https://github.com/LyricTian/gin-admin
   e, err := InitializeEvent(msg)
   if err != nil {
      fmt.Printf("failed to create event: %s\n", err)
      os.Exit(2)
   }
   e.start()
}
上面的更改主要是NewPeople 函数增加了msg参数,同时InitializeEvent增加了msg参数,这个时候我们通过wire生成代码则可以看到如下:
// Injectors from main.go:
func InitializeEvent(msg string) (Event, error) {
	message := NewMessage(msg)
	people := NewPeople(message)
	event, err := NewEvent(people)
	if err != nil {
		return Event{}, err
	}
	return event, nil
}
wire 会检查注入器的参数,并检查到NewMessage 需要msg的参数,所以它将msg传递给了NewMessage
栗子4
如果我们传给wire.Build 的依赖关系存在问题,wire会怎么处理呢? 我们调整InitializeEvent 的代码:
func InitializeEvent(msg string) (Event, error) {
   wire.Build(NewEvent, NewMessage)
   return Event{}, nil
}
然后执行wire 进行代码的生成:
➜  useWireBaseExample4 wire .
wire: /home/fan/codes/go_project/awesomeProject/202006/useWireBaseExample4/main.go:63:1: inject InitializeEvent: no provider found for awesomeProject/202006/useWireBaseExample4.People
        needed by awesomeProject/202006/useWireBaseExample4.Event in provider "NewEvent" (/home/fan/codes/go_project/awesomeProject/202006/useWireBaseExample4/main.go:46:6)
wire: awesomeProject/202006/useWireBaseExample4: generate failed
wire: at least one generate failure
➜  useWireBaseExample4 
错误提示中非常清楚的告诉我它找不到no provider found ,如果我们传给wire.Build 没有用的依赖,它依然会给我们提示告诉我们 unused provider "main.NewEventNumber"
➜  useWireBaseExample4 wire .
wire: /home/fan/codes/go_project/awesomeProject/202006/useWireBaseExample4/main.go:67:1: inject InitializeEvent: unused provider "main.NewEventNumber"
wire: awesomeProject/202006/useWireBaseExample4: generate failed
wire: at least one generate failure
wire的高级用法
Binding Interfaces
依赖注入通常用于绑定接口的具体实现。通过下面的例子理解:
// Run 运行服务
func Run(ctx context.Context, opts ...Option) error {
	var state int32 = 1
	sc := make(chan os.Signal, 1)
	signal.Notify(sc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
	cleanFunc, err := Init(ctx, opts...)
	if err != nil {
		return err
	}
EXIT:
	for {
		sig := <-sc
		logger.Printf(ctx, "接收到信号[%s]", sig.String())
		switch sig {
		case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
			atomic.CompareAndSwapInt32(&state, 1, 0)
			break EXIT
		case syscall.SIGHUP:
		default:
			break EXIT
		}
	}
	cleanFunc()
	logger.Printf(ctx, "服务退出")
	time.Sleep(time.Second)
	os.Exit(int(atomic.LoadInt32(&state)))
	return nil
}package main
import (
	"fmt"
	"github.com/google/wire"
)
type Fooer interface {
	Foo() string
}
type MyFooer string
func (b *MyFooer) Foo() string {
	return string(*b)
}
func provideMyFooer() *MyFooer {
	b := new(MyFooer)
	*b = "Hello, World!"
	return b
}
type Bar string
func provideBar(f Fooer) string {
	// f will be a *MyFooer.
	return f.Foo()
}
func InitializeEvent() string {
	wire.Build(provideMyFooer, provideBar, wire.Bind(new(Fooer), new(*MyFooer)))
	return ""
}
func main() {
	ret := InitializeEvent()
	fmt.Println(ret)
}
我们可以看到Fooer 是一个interface, MyFooer 实现了Fooer 这个接口,同时provideBar 的参数是Fooer 接口类型。可以看到// Run 运行服务
func Run(ctx context.Context, opts ...Option) error {
	var state int32 = 1
	sc := make(chan os.Signal, 1)
	signal.Notify(sc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
	cleanFunc, err := Init(ctx, opts...)
	if err != nil {
		return err
	}
EXIT:
	for {
		sig := <-sc
		logger.Printf(ctx, "接收到信号[%s]", sig.String())
		switch sig {
		case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
			atomic.CompareAndSwapInt32(&state, 1, 0)
			break EXIT
		case syscall.SIGHUP:
		default:
			break EXIT
		}
	}
	logger.Printf(ctx, "服务退出")
	time.Sleep(time.Second)
	os.Exit(int(atomic.LoadInt32(&state)))
	return nil
}
代码中我们用了wire.Bind方法,为什么这么用呢?如果我们wire.Build的那段代码写成如下:
wire.Build(provideMyFooer, provideBar),再次用wire生成代码则会提示如下错误:https://github.com/LyricTian/gin-admin
➜  useWireBaseExample5 wire .
wire: /home/fan/codes/go_project/awesomeProject/202006/useWireBaseExample5/main.go:36:1: inject InitializeEvent: no provider found for awesomeProject/202006/useWireBaseExample5.Fooer
        needed by string in provider "provideBar" (/home/fan/codes/go_project/awesomeProject/202006/useWireBaseExample5/main.go:27:6)
wire: awesomeProject/202006/useWireBaseExample5: generate failed
wire: at least one generate failure
这是因为我们传递给provideBar 需要的是 Fooer 接口类型,我们传给wire.Build 的是provideMyFooer, provideBar 这个时候默认从依赖关系里,provideBar 没有找能够提供Fooer的provider, 虽然我们我们都知道MyFooer 实现了Fooer 这个接口。所以我们需要在wire.Build 里告诉它,我们传递provideMyFooer 就是provideBar的provider。wire.Bind 就是来做这件事情的。
wire.Bind 的第一个参数是接口类型的值的指针,第二个参数是实现第一个参数接口的类型的值的指针。
这样当我们在用wire生成代码的时候就正常了。
Struct Providers
wire还可以用于结构体的构造。先直接看使用的例子:
package main
import (
   "fmt"
   "github.com/google/wire"
)
type Foo int
type Bar int
func ProvideFoo() Foo {
   return Foo(1)
}
func ProvideBar() Bar {
   return Bar(2)// Run 运行服务
func Run(ctx context.Context, opts ...Option) error {
	var state int32 = 1
	sc := make(chan os.Signal, 1)
	signal.Notify(sc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
	cleanFunc, err := Init(ctx, opts...)
	if err != nil {
		return err
	}
EXIT:
	for {
		sig := <-sc
		logger.Printf(ctx, "接收到信号[%s]", sig.String())
		switch sig {
		case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
			atomic.CompareAndSwapInt32(&state, 1, 0)
			break EXIT
		case syscall.SIGHUP:
		default:
			break EXIT
		}
	}
	cleanFunc()
	logger.Printf(ctx, "服务退出")
	time.Sleep(time.Second)
	os.Exit(int(atomic.LoadInt32(&state)))
	return nil
}
}
type FooBar struct {
   MyFoo Foo
   MyBar Bar
}
var Set = wire.NewSet(
   ProvideFoo,
   ProvideBar,
   wire.Struct(new(FooBar), "MyFoo", "MyBar"),
)
func injectFooBar() FooBar {
   wire.Build(Set)
   return FooBar{}
}
func main() {
   fooBar := injectFooBar()
   fmt.Println(fooBar)
}
上面的例子其实很简单,我们构造FooBar 结构题我们需要MyFoo 和 MyBar ,而ProvideFoo 和 ProvideBar 就是用于生成MyFoo 和 MyBar,wire.Struct  也可以帮我们做这件事情。我们通过wire生成的代码如下:
// Injectors from main.go:
func injectFooBar() FooBar {
   foo := ProvideFoo()
   bar := ProvideBar()
   fooBar := FooBar{
      MyFoo: foo,
      MyBar: bar,
   }
   return fooBar
}
wire.Struct 的第一个参数是所需结构类型的指针,后面的参数是要注入的字段的名称。可以使用一个特殊的字符串“ * ”作为告诉注入器注入所有字段的快捷方式。 所以我们上面的代码也可以写成:wire.Struct(new(FooBar), "×") ,而当我们使用* 这种方式的时候可能会把一些不需要注入的字段注入了,如锁,那么类似这种情况,如果我们注入,卡一通过wire:"-" 的方式告诉wire 该字段不进行注入。
type Foo struct {
    mu sync.Mutex `wire:"-"`
    Bar Bar
}
Binding Values
这个功能主要就是给数据类型绑定一个默认值,代码例子如下:
https://github.com/LyricTian/gin-adminpackage main
import (
   "fmt"
   "github.com/google/wire"
)
type Foo struct {
   X int
}
func injectFoo() Foo {
   wire.Build(wire.Value(Foo{X: 11}))
   return Foo{}
}
func main() {
   foo := injectFoo()
   fmt.Println(foo)
}
我通过wire生成的代码如下:
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
package main
import (
   "fmt"
)
// Injectors from main.go:
func injectFoo() Foo {
   foo := _wireFooValue
   return foo
}
var (
   _wireFooValue = Foo{X: 11}
)
// main.go:
type Foo struct {
   X int
}
func main() {
   foo := injectFoo()
   fmt.Println(foo)
}
Use Fields of a Struct as Providers
有时,我们需要获取结构体的某些字段,按照我们已经使用的wire的用法,你可能会这样写代码:
package main
import (
   "fmt"
   "github.com/google/wire"
)
type Foo struct {
   S string
   N int
   F float64
}
func getS(foo Foo) string {
   return foo.S
}
func provideFoo() Foo {
   return Foo{S: "Hello, World!", N: 1, F: 3.14}
}
func injectedMessage() string {
   wire.Build(
      provideFoo,
      getS,
   )
   return ""
}
func main() {
   ret := injectedMessage()
   fmt.Println(ret)
}
这种用法当然也可以实现,但是wire其实提供了更好的办法来实现wire.FieldsOf, 我们将上面的代码进行更改如下,通过wire生成的代码其实和上面的是一样的:
package main
import (
   "fmt"
   "github.com/google/wire"
)
type Foo struct {
   S string
   N int
   F float64
}
func provideFoo() Foo {
   return Foo{S: "Hello, World!", N: 1, F: 3.14}
}
func injectedMessage() string {
   wire.Build(
      provideFoo,
      wire.FieldsOf(new(Foo), "S"),
   )
   return ""
}
func main() {
   ret := injectedMessage()
   fmt.Println(ret)
}
Cleanup functions
如果我们的Provider创建了一个需要做clean 的值,例如关闭文件,关闭数据连接..., 这里也是可以返回一个闭包来清理资源,注入器将使用它向调用者返回一个聚合的清理函数,或者如果稍后在注入器实现中调用的提供程序返回一个错误,则使用它来清理资源。
关于这个功能的使用,通过https://github.com/LyricTian/gin-admin 的代码中的使用,可以更加清楚。
作者在gin-admin/internal/app/app.go 中进行了初始化依赖注入器
// 初始化依赖注入器
injector, injectorCleanFunc, err := injector.BuildInjector()
if err != nil {
   return nil, err
}
我们在看看下wire生成的wire_gen.go代码:
// Injectors from wire.go:
func BuildInjector() (*Injector, func(), error) {
   auther, cleanup, err := InitAuth()
   if err != nil {
      return nil, nil, err
   }
   db, cleanup2, err := InitGormDB()
   if err != nil {
      cleanup()
      return nil, nil, err
   }
   role := &model.Role{
      DB: db,
   }
   roleMenu := &model.RoleMenu{
      DB: db,
   }
   menuActionResource := &model.MenuActionResource{
      DB: db,
   }
   user := &model.User{
      DB: db,
   }
   userRole := &model.UserRole{
      DB: db,
   }
   casbinAdapter := &adapter.CasbinAdapter{
      RoleModel:         role,
      RoleMenuModel:     roleMenu,
      MenuResourceModel: menuActionResource,
      UserModel:         user,
      UserRoleModel:     userRole,
   }
   syncedEnforcer, cleanup3, err := InitCasbin(casbinAdapter)
   if err != nil {
      cleanup2()
      cleanup()
      return nil, nil, err
   }
   demo := &model.Demo{
      DB: db,
   }
   bllDemo := &bll.Demo{
      DemoModel: demo,
   }
   apiDemo := &api.Demo{
      DemoBll: bllDemo,
   }
   menu := &model.Menu{
      DB: db,
   }
   menuAction := &model.MenuAction{
      DB: db,
   }
   login := &bll.Login{
      Auth:            auther,
      UserModel:       user,
      UserRoleModel:   userRole,
      RoleModel:       role,
      RoleMenuModel:   roleMenu,
      MenuModel:       menu,
      MenuActionModel: menuAction,
   }
   apiLogin := &api.Login{
      LoginBll: login,
   }
   trans := &model.Trans{
      DB: db,
   }
   bllMenu := &bll.Menu{
      TransModel:              trans,
      MenuModel:               menu,
      MenuActionModel:         menuAction,
      MenuActionResourceModel: menuActionResource,
   }
   apiMenu := &api.Menu{
      MenuBll: bllMenu,
   }
   bllRole := &bll.Role{
      Enforcer:      syncedEnforcer,
      TransModel:    trans,
      RoleModel:     role,
      RoleMenuModel: roleMenu,
      UserModel:     user,
   }
   apiRole := &api.Role{
      RoleBll: bllRole,
   }
   bllUser := &bll.User{
      Enforcer:      syncedEnforcer,
      TransModel:    trans,
      UserModel:     user,
      UserRoleModel: userRole,
      RoleModel:     role,
   }
   apiUser := &api.User{
      UserBll: bllUser,
   }
   routerRouter := &router.Router{
      Auth:           auther,
      CasbinEnforcer: syncedEnforcer,
      DemoAPI:        apiDemo,
      LoginAPI:       apiLogin,
      MenuAPI:        apiMenu,
      RoleAPI:        apiRole,
      UserAPI:        apiUser,
   }
   engine := InitGinEngine(routerRouter)
   injector := &Injector{
      Engine:         engine,
      Auth:           auther,
      CasbinEnforcer: syncedEnforcer,
      MenuBll:        bllMenu,
   }
   return injector, func() {
      cleanup3()
      cleanup2()
      cleanup()
   }, nil
}
而当程序退出的时候这上面代码返回的那些清理操作都会被执行:
// Run 运行服务
func Run(ctx context.Context, opts ...Option) error {
   var state int32 = 1
   sc := make(chan os.Signal, 1)
   signal.Notify(sc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
   cleanFunc, err := Init(ctx, opts...)
   if err != nil {
      return err
   }
EXIT:
   for {
      sig := <-sc
      logger.Printf(ctx, "接收到信号[%s]", sig.String())
      switch sig {
      case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
         atomic.CompareAndSwapInt32(&state, 1, 0)
         break EXIT
      case syscall.SIGHUP:
      default:
         break EXIT
      }
   }
   // 在这里执行了清理工作
   cleanFunc()
   logger.Printf(ctx, "服务退出")
   time.Sleep(time.Second)
   os.Exit(int(atomic.LoadInt32(&state)))
   return nil
}
延伸阅读
从别人的代码中学习golang系列--02的更多相关文章
- 从别人的代码中学习golang系列--01
		
自己最近在思考一个问题,如何让自己的代码质量逐渐提高,于是想到整理这个系列,通过阅读别人的代码,从别人的代码中学习,来逐渐提高自己的代码质量.本篇是这个系列的第一篇,我也不知道自己会写多少篇,但是希望 ...
 - 从别人的代码中学习golang系列--03
		
这篇博客还是整理从https://github.com/LyricTian/gin-admin 这个项目中学习的golang相关知识. 作者在项目中使用了 github.com/casbin/casb ...
 - 在C#代码中应用Log4Net系列教程
		
在C#代码中应用Log4Net系列教程(附源代码) Log4Net应该可以说是DotNet中最流行的开源日志组件了.以前需要苦逼写的日志类,在Log4Net中简单地配置一下就搞定了.没用过Log4 ...
 - Python--网络编程学习笔记系列02 附:tcp服务端,tcp客户端
		
Python--网络编程学习笔记系列02 TCP和UDP的概述: udp通信模型类似于写信,不需要建立相关链接,只需要发送数据即可(现在几乎不用:不稳定,不安全) tcp通信模型类似于打电话,一定要建 ...
 - 在C#代码中应用Log4Net系列教程(附源代码)
		
Log4Net应该可以说是DotNet中最流行的开源日志组件了.以前需要苦逼写的日志类,在Log4Net中简单地配置一下就搞定了.没用过Log4Net,真心不知道原来日志组件也可以做得这么灵活,当然这 ...
 - [转]在C#代码中应用Log4Net系列教程(附源代码)
		
Log4Net应该可以说是DotNet中最流行的开源日志组件了.以前需要苦逼写的日志类,在Log4Net中简单地配置一下就搞定了.没用过Log4Net,真心不知道原来日志组件也可以做得这么灵活,当然这 ...
 - NeteaseCloudWebApp模仿网易云音乐的vue自己从开源代码中学习到的
		
github地址: https://github.com/javaSwing/NeteaseCloudWebApp 1.Vue.prototype.$http = Axios // 类似于vue-re ...
 - 在C#代码中应用Log4Net系列教程(附源代码)地址
		
在博客园看到一篇关于Log4Net使用教程,比较详细,感谢这位热心的博主 博客园地址:http://www.cnblogs.com/kissazi2/archive/2013/10/29/339359 ...
 - WebService学习笔记系列(二)
		
soap(简单对象访问协议),它是在http基础之上传递xml格式数据的协议.soap协议分为两个版本,soap1.1和soap1.2. 在学习webservice时我们有一个必备工具叫做tcpmon ...
 
随机推荐
- Linux 系统定时任务
			
crond服务管理与访问控制 启动 service crond restart 开机自启动 chkconfig crond on 用户的crontab设置 crontab [选项],其中 -e :编辑 ...
 - awardRotate转盘插件文字模糊问题和图片加载问题
			
前言 最近在做一个转盘抽奖页面,使用了awardRotate.js发现字体和图片都有模糊,绘制的时候图片绘制不全,搜索一下之后发现针对awardRotate的解决方法比较少,针对canvas的比较多, ...
 - 彻底搞懂 etcd 系列文章(二):etcd 的多种安装姿势
			
0 专辑概述 etcd 是云原生架构中重要的基础组件,由 CNCF 孵化托管.etcd 在微服务和 Kubernates 集群中不仅可以作为服务注册与发现,还可以作为 key-value 存储的中间件 ...
 - Python快速入门文档
			
前言 此文本质为本人学习Python过程中的笔记,部分地方叙述表达可能不够清晰,欢迎留言. (本文适合有一定程序语言基础的读者阅读(最好是c语言)) 一.基本语法 1.框架: (1)以缩进表示层次所属 ...
 - OC语言-NSMutableArray为什么要用strong来修饰
			
Talk is cheap show you my code! NSMutableArray属性为什么要用strong来修饰,其实就是一个深复制和浅复制的问题. <pre name=" ...
 - ELK 收集交换机日志(以华为交换机为例)
			
大概思路 交换机日志----> 服务器---->服务器rsyslog设置指定存储路径文件--->随后就跟elk 监控本机日志一样了 huawei switch: #指定发送消息基本, ...
 - js实现点击切换checkbox背景图片
			
在制作网页的过程中,经常需要实现点击切换背景图片的效果,本文关注的是表单中checkbox背景图片切换的实现.如图,在输入用户名和密码后,用户需要选择是否记住密码 图片背景为白色表示未勾选状态 ,背景 ...
 - Java操作符,<<、>>等
			
数学意义:在数字没有溢出的前提下,对于正数和负数,左移一位都相当于乘以2的1次方,左移n位就相当于乘以2的n次方. 计算:3 << 2 3 << 2,则是将数字3左移2位 1. ...
 - 若linux 的分区硬盘满,如何处理?
			
一.确定是不是真的是磁盘空间不足 输入命令:df –lh 查看磁盘信息 二.如何定位最大文件目录 输入命令:cd / 进入根目录. 输入命令:du -h max-depth=1 寻找当前目录,哪个文件 ...
 - Win10下Tensorflow的安装
			
Win10下Tensorflow的安装 1. Tensorflow简介 TensorFlow是谷歌基于DistBelief进行研发的第二代人工智能学习系统,其命名来源于本身的运行原理.Tensor(张 ...