当前位置: 首页 > 编程日记 > 正文

Golang微服务开发实践

github: github.com/yun-mu/Micr…

微服务概念学习:可参考 Nginx 的微服务文章

微服务最佳实践:可参考 微服务最佳实践

demo 简介

服务:

  • consignment-service(货运服务)
  • user-service(用户服务)
  • log-service (日志服务)
  • vessel-service(货船服务)
  • api-service (API 服务)

用到的技术栈如下:

framework: go-micro, gin
Transport: tcp
Server: rpc
Client: rpc
RegisterTTL: 30s
RegisterInterval: 20s
Registry: consul, 服务发现和注册
Broker: kafka, 消息队列
Selector: cache, 负载均衡
Codec: protobuf, 编码
Tracing: jaeger, 链路追踪
Metrics: jaeger
breaker: hystrix, 熔断
ratelimit: uber/ratelimit, 限流
复制代码

服务关系图

实体关系图

服务流程示例

认证

采用 JWT

发布订阅模式

demo 运行

前提工具:go, dep, docker, docker-compose, mongo

首先初始化:make init

Makefile 部分代码如下:

init:cd ..mv MicroServicePractice ${GOPATH}/src/Ethan/./pull.sh # 安装 go 依赖cd pluginsdocker-compose -f docker-compose.yml up -d # 安装插件,如:kafka, consul, zookeeper, jaeger
复制代码

之后就可以运行代码了:

注:建议自己开多个终端 go run ,这样可以看日志

make run # 允许 服务 server
复制代码

测试:

注:注意顺序,刚开始啥数据都没有的

go run user-cli/cli.go
export Token=$Token # 注意换成前面生成的Token
go run vessel-cli/cli.go
go run consignment-cli/cli.go
复制代码

开发详解

proto 代码生成

安装工具:

protoc 安装:google.github.io/proto-lens/…

protoc-gen-goprotoc-gen-micro

go get -u -v google.golang.org/grpc				
go get -u -v github.com/golang/protobuf/protoc-gen-go
go get -u -v github.com/micro/protoc-gen-micro
复制代码

生成的脚本我已经写好 Makefile, 进入 interface-center 目录,执行make build 即可

内部示例如下:

protoc --proto_path=proto:. --go_out=plugins=micro:out/ proto/vessel/vessel.proto
复制代码

这里使用 micro 插件,若想和不使用插件对比,可使用如下命令:

protoc --proto_path=proto:. --go_out=out/ --micro_out=out/ proto/vessel/vessel.proto
复制代码

这样会生成两个文件,一个为 .micro.go 一个为 .pb.go

这里顺便看一下 生成的 pb 文件里是如何进行 rpc 调用的,我们随便看一个 方法,如:vesselFindAvailable

func (c *vesselServiceClient) FindAvailable(ctx context.Context, in *Specification, opts ...client.CallOption) (*Response, error) {req := c.c.NewRequest(c.serviceName, "VesselService.FindAvailable", in)out := new(Response)err := c.c.Call(ctx, req, out, opts...)if err != nil {return nil, err}return out, nil
}
复制代码
func newRequest(service, endpoint string, request interface{}, contentType string, reqOpts ...RequestOption) Request {var opts RequestOptionsfor _, o := range reqOpts {o(&opts)}// set the content-type specifiedif len(opts.ContentType) > 0 {contentType = opts.ContentType}return &rpcRequest{service:     service,method:      endpoint,endpoint:    endpoint,body:        request,contentType: contentType,opts:        opts,}
}
复制代码

微服务开发流程

如果使用 grpc 作为 serverclient,开发流程如下:

注:serverclient 必须相同,如:我的代码中 serverclient 使用的都是 rpc, transporttcp

目录简介

  • api:对外暴露的HTTP web 接口,可以理解为 网关
  • common:所有服务都能调用的东西,如 GetMicroClient, GetMicroServer
  • config:配置中心,其他服务的启动都依赖的配置
  • consignment
  • consignment-cli:cli 测试
  • interface-center:proto 文件中心,同时生成的 .go 文件也在这里
  • shippy-ui:前端测试 ui 代码,对接API,API还没写完
  • user
  • user-cli
  • vessel
  • vessel-cli

初始化

示例代码:consignment/main.go, common/service.go

// 直接调用自己写的公有的库获取 server,保持配置同步	
// common.AuthWrapper 为前置认证,采用JWT
srv := common.GetMicroServer(service, micro.WrapHandler(common.AuthWrapper))
复制代码

common.GetMicroServer

func GetMicroServer(service string, exOpts ...micro.Option) micro.Service {opts := getOpts(service)if defaultServer != nil {opts = append(opts, defaultServer)}// ...// 注意顺序,同样的配置后面的会将前面的覆盖opts = append(opts, exOpts...)srv := micro.NewService(opts...)// 初始化,解析命令行参数srv.Init()return srv
}
复制代码

注:调用者的 client, transport 应当和 server 的 client, transport 配置相同,所以开发 micro web的时候要注意!micro web 全是 HTTP 或者 ws,需要自己使用和后面服务相同的 client 来完成转发。

服务注册

这里我的demo中采用了 consul,consul 自带了 UI和健康检查,consul UI 端口为:8500

// 注册延迟,30s 内没有注册则失效,consul 会自动删除服务micro.RegisterTTL(time.Second * 30),
// 注册间隔,每隔 20s 注册一次micro.RegisterInterval(time.Second * 20)
// ...
// opts 中添加如下配置即可
micro.Registry(consul.NewRegistry(func(op *registry.Options) {op.Addrs = config.GetRegistryAddrs(service)}))
复制代码

docker-compose.yml 中已经定义,这里测试用,因此只采用单节点(server)的形式,consul 采用 Raft 算法,为了保证选主无误,节点(server)数必须是奇数,bootstrap-expect 表示节点数量

  consul:
    image: consul:1.5
    container_name: consul-node1
    command: agent -server -bootstrap-expect=1 -node=node1 -bind=0.0.0.0 -client=0.0.0.0 -datacenter=dc1
    volumes:
      - ./consul/node1:/consul/data  consul-client:
    image: consul:1.5
    container_name: consul-client1
    command: agent -retry-join=consul -node=client1 -bind=0.0.0.0 -client=0.0.0.0 -datacenter=dc1 -ui
    ports:
      - "8500:8500"
      - "8600:8600"
      - "8300:8300"
    depends_on:
      - consul
    volumes:
      - ./consul/client1:/consul/data
复制代码

watch

	// 这里的 handler 应当实现 pb 中定义的调用h := handler.GetHandler(session, vClient, uClient, bk)	// 将 server 作为微服务的服务端pb.RegisterShippingServiceHandler(srv.Server(), h)if err := srv.Run(); err != nil {log.Fatalf("failed to serve: %v", err)}
复制代码

pb

// 货轮微服务
service ShippingService {// 托运一批货物rpc CreateConsignment (Consignment) returns (Response) {}// 查看托运货物的信息rpc GetConsignments (GetRequest) returns (Response) {}
}
复制代码

handler

func (h *Handler) CreateConsignment(ctx context.Context, req *pb.Consignment, resp *pb.Response) error {
}
func (h *Handler) GetConsignments(ctx context.Context, req *pb.GetRequest, resp *pb.Response) error {
}
复制代码

上下文 context

在我们的 AuthWrapper 中,ctx 作为上下文信息传递的方式,可在 ctx 中添加信息

//
//  AuthWrapper 是一个高阶函数,入参是 "下一步" 函数,出参是认证函数
// 在返回的函数内部处理完认证逻辑后,再手动调用 fn() 进行下一步处理
// token 是从 上下文中取出的,再调用 user-service 将其做验证
// 认证通过则 fn() 继续执行,否则报错
//
func AuthWrapper(fn server.HandlerFunc) server.HandlerFunc {log.Println("AuthWrapper")return func(ctx context.Context, req server.Request, resp interface{}) error {// consignment-service 独立测试时不进行认证if os.Getenv("DISABLE_AUTH") == "true" {return fn(ctx, req, resp)}meta, ok := metadata.FromContext(ctx)if !ok {return errors.New("no auth meta-data found in request")}token := meta["token"]// Auth hereauthResp, err := GetUserClient().ValidateToken(context.Background(), &userPb.Token{Token: token,})log.Println("Auth Resp:", authResp)if err != nil {return err}// 这里将 JWT 解析出来的 user_id 传递下去ctx = context.WithValue(ctx, "user_id", authResp.UserId)err = fn(ctx, req, resp)return err}
}复制代码

handler

在所有前置操作执行完毕之后,开始执行 handler 真正的 Call,handler 的函数定义必须和 pb 中一模一样。

处理完之后直接编辑 resp 即可,之后返回 nil,resp 是一个指针,直接传递了返回信息。

func (h *Handler) CreateConsignment(ctx context.Context, req *pb.Consignment, resp *pb.Response) error {// ... 处理resp.Created = trueresp.Consignment = req// 后置操作go func() {// ...h.pubLog(userID, "CreateConsignment", msg)}()return nil
}
复制代码

过滤器

这里以版本过滤器为例:

// Filter will filter the version of the service
func Filter(v string) client.CallOption {if v == "" {v = "latest"}filter := func(services []*registry.Service) []*registry.Service {var filtered []*registry.Servicefor _, service := range services {if service.Version == v {filtered = append(filtered, service)}}return filtered}return client.WithSelectOption(selector.WithFilter(filter))
}复制代码

之后在进行 client.Call 的时候可以使用

vResp, err := h.vesselClient.FindAvailable(ctx, vReq, common.Filter(version))
复制代码

db 交互

一般而言不要把 pb 结构体直接插入 数据库中,最好有一个 中间转换层。示例如下:

func (repo *ConsignmentRepository) Create(con *pb.Consignment) error {// 这里将PB转换为想要的结构体,之后再插入data := PBConsignment2Consignment(con)// dao 层直接对接DB操作return dao.Insert(repo.collection(), &data)
}// 在外面记得把 Session Close
复制代码

broker

消息队列

// common/service.go/GetMicroServer()
// 注册brokerKafka := kafka.NewBroker(func(options *broker.Options) {// eg: []{"127.0.0.1:9092"}options.Addrs = config.GetBrokerAddrs(service)})if err := brokerKafka.Connect(); err != nil {log.Fatalf("Broker Connect error: %v", err)}
// ... micro.Broker(brokerKafka)
复制代码

注册完之后就开始定义 接口了

	// 这里我将 kafka broker 传入 handler 中bk := srv.Server().Options().Brokerh := handler.GetHandler(session, vClient, uClient, bk)
复制代码

发布消息:

// 发送log// ...data := &broker.Message{Header: map[string]string{"user_id": userID,},Body: body,}if err := h.Broker.Publish(topic, data); err != nil {log.Printf("[pub] failed: %v\n", err)}
复制代码

订阅消息:

	bk := srv.Server().Options().Broker// 这里订阅了 一个 topic, 并提供接口处理_, err := bk.Subscribe(topic, subLog)
复制代码
func subLog(pub broker.Publication) error {var logPB *pb.Log// 自行解析 body 即可if err := json.Unmarshal(pub.Message().Body, &logPB); err != nil {return err}log.Printf("[Log]: user_id: %s,  Msg: %v\n", pub.Message().Header["user_id"], logPB)return nil
}复制代码

熔断

Micro提供了两种实现,gobreaker和hystrix,熔断是在客户端实现。

来看看 hystrix:

var (// DefaultTimeout is how long to wait for command to complete, in millisecondsDefaultTimeout = 1000// DefaultMaxConcurrent is how many commands of the same type can run at the same timeDefaultMaxConcurrent = 10// DefaultVolumeThreshold is the minimum number of requests needed before a circuit can be tripped due to healthDefaultVolumeThreshold = 20// DefaultSleepWindow is how long, in milliseconds, to wait after a circuit opens before testing for recoveryDefaultSleepWindow = 5000// DefaultErrorPercentThreshold causes circuits to open once the rolling measure of errors exceeds this percent of requestsDefaultErrorPercentThreshold = 50// DefaultLogger is the default logger that will be used in the Hystrix package. By default prints nothing.DefaultLogger = NoopLogger{}
)type Settings struct {Timeout                time.DurationMaxConcurrentRequests  intRequestVolumeThreshold uint64SleepWindow            time.DurationErrorPercentThreshold  int
}
复制代码

若想修改参数,hystrix 没有提供全局的接口修改,这里我直接修改默认参数

//  "github.com/afex/hystrix-go/hystrix"	hystrix.DefaultMaxConcurrent = 100hystrix.DefaultVolumeThreshold = 50
复制代码

注册:

// "github.com/micro/go-plugins/wrapper/breaker/hystrix"
// 添加如下配置即可
micro.WrapClient(hystrix.NewClientWrapper()
复制代码

限流

ratelimit 可以在客户端做,也可以在服务端做;micro提供了两种方案:juju/ratelimituber/ratelimit

我们看看 uber 的:

// "github.com/micro/go-plugins/wrapper/ratelimiter/uber"
// 添加如下配置即可
// ratelimit 的配置可自行查看 API 修改
micro.WrapClient(ratelimit.NewHandlerWrapper(1024))
复制代码

链路追踪

这里使用 jaeger , jaeger 提供了UI界面,端口为16686

docker-compose.yml 中已经定义

  jaeger:
    image: jaegertracing/all-in-one:1.12
    container_name: tracing
    environment:
      COLLECTOR_ZIPKIN_HTTP_PORT: 9411
    ports:
      - "5775:5775/udp"
      - "6831:6831/udp"
      - "6832:6832/udp"
      - "5778:5778"
      - "16686:16686"
      - "14268:14268"
      - "9411:9411"
复制代码
func NewJaegerTracer(serviceName, addr string) (opentracing.Tracer, io.Closer, error) {// Sample configuration for testing. Use constant sampling to sample every trace// and enable LogSpan to log every span via configured Logger.cfg := jaegercfg.Configuration{Sampler: &jaegercfg.SamplerConfig{Type:  jaeger.SamplerTypeConst,Param: 1,},Reporter: &jaegercfg.ReporterConfig{LogSpans: true,BufferFlushInterval: 1 * time.Second,},}cfg.ServiceName = serviceName// Example logger and metrics factory. Use github.com/uber/jaeger-client-go/log// and github.com/uber/jaeger-lib/metrics respectively to bind to real logging and metrics// frameworks.jLogger := &jaegerLogger{}jMetricsFactory := metrics.NullFactorymetricsFactory := metrics.NullFactorymetrics := jaeger.NewMetrics(metricsFactory, nil)sender, err := jaeger.NewUDPTransport(addr, 0)if err != nil {log.Logf("could not initialize jaeger sender: %s", err.Error())return nil, nil, err}repoter := jaeger.NewRemoteReporter(sender, jaeger.ReporterOptions.Metrics(metrics))return cfg.NewTracer(jaegercfg.Logger(jLogger),jaegercfg.Metrics(jMetricsFactory),jaegercfg.Reporter(repoter),)}type jaegerLogger struct{}func (l *jaegerLogger) Error(msg string) {log.Logf("ERROR: %s", msg)
}// Infof logs a message at info priority
func (l *jaegerLogger) Infof(msg string, args ...interface{}) {log.Logf(msg, args...)
}
复制代码
// 	"github.com/micro/go-plugins/wrapper/trace/opentracing"
opentracing.NewClientWrapper(t)
复制代码

API

这里结合大名鼎鼎的 HTTP Restful 框架 gin 使用

主要代码如下:

	// web 的初始化不太一样 micro -> websrv := common.GetMicroWeb(service)// ...router := gin.Default()// ... 正常 gin router 绑定操作// 最后直接将 服务 / 绑定到 gin 的router上,交给 gin 处理srv.Handle("/", router)
复制代码

go-micro 详解

micro 文档:micro.mu/docs/index.…

参见另一篇 go-micro详解

micro

参见另一篇 micro 工具箱

转载于:https://juejin.im/post/5cfa1b5b6fb9a07ecf721696

相关文章:

Linux ssh/scp/docker学习

文章目录Linux ssh/scp/docker使用学习1. ssh 登录2. scp传输文件3. docker4. git checkout 替换指定分支的单个文件Linux ssh/scp/docker使用学习 1. ssh 登录 sudo ssh fireflyip (登录账号密码) scp -r company/data_depthfill/ firefly192.168.105.6:/tmp/ ​​​​ 2…

tensorflow mnist 1

import tensorflow as tf from tensorflow.examples.tutorials.mnist import input_data import keras.backend.tensorflow_backend as KTFdef add_layer(inputs,in_size,out_size,activation_functionNone):#Weights是一个矩阵,[行,列]为[in_size,out_s…

framework7使用笔记

2019独角兽企业重金招聘Python工程师标准>>> myApp.addView(.view-main, {}); 以上这句代码一定要添加 ,否则链接的页面不能正常加载 --------------------------------------------- 如果初始化时定义了preprocess,则页面上链接的自动加载将…

Linux:检查当前运行级别的五种方法

2019独角兽企业重金招聘Python工程师标准>>> 运行级就是Linux操作系统当前正在运行的功能级别。存在七个运行级别,编号从0到6。系统可以引导到任何给定的运行级别。运行级别由数字标识。每个运行级别指定不同的系统配置,并允许访问不同的进程…

概率机器人资料整理

机器人算法仿真 https://atsushisakai.github.io/PythonRobotics/ 最大熵对应的概率分布及其优化推导 https://www.cnblogs.com/yychi/p/9401807.html 矩阵计算网址 http://www.yunsuan.info/matrixcomputations/index.html

LLVM官方文档翻译---- LLVM原子指令与并发指引

英文原文地址:http://llvm.org/docs/Atomics.html 译文原文地址:http://blog.csdn.net/wuhui_gdnt/article/details/52485591 注:该文章转载已经得到译者授权。 --------------------------------------------------------------------------…

matplotlib画图

import matplotlib.pyplot as plt import numpy as npdef test1():# 从[-1,1]中等距去50个数作为x的取值x np.linspace(-1, 1, 50)print(x)y 2*x 1y1 2**x 1# 第一个是横坐标的值,第二个是纵坐标的值plt.plot(x, y)plt.plot(x, y1)# 必要方法,用于将…

学习新对象字面量语法

目标 使用简写属性名称使用简写方法名称使用计算属性名称问题 哪些部分是冗余的? const person{name:name,address:address,sayHello:function(){/*......*/},sayName:function(){/*......*/}sayAddress:function(){/*......*/}} 复制代码简写属性名称 //ES5 const message{te…

ORB-SLAM2代码/流程详解

ORB-SLAM2代码详解 文章目录ORB-SLAM2代码详解1. ORB-SLAM2代码详解01_ORB-SLAM2代码运行流程1 运行官方Demo1.2. 阅读代码之前你应该知道的事情1.2.1 变量命名规则1.3 理解多线程1.3.1 为什么要使用多线程?1.3.2 多线程中的锁1.4 SLAM主类System1.4.1 System类是ORB-SLAM2系统…

VS2010 CUDA 5.5 Win7 64位配置以及项目创建配置

一.安装CUDA5.5以及配置VS助手 1、安装之前必须确认自己电脑的GPU支持CUDA。在设备管理器中找到显示适配器(Display adapters),找到自己电脑的显卡型号,如果包含在http://www.nvidia.com/object/cuda_gpus.html的列表中,说明支持…

HTC VIVE SDK 中的例子 hellovr_opengl 程序流程分析

最近Vive的VR头盔设备很火,恰逢项目需求,所以对 SDK 中的例子 hellovr_opengl 做了比较细致的代码分析,先将流程图绘制如下,便于大家理解。 在ViVe头盔中实现立体效果的技术核心: 如果要外挂Vive的VR设备实现立体效果&…

Proximal Algorithms 4 Algorithms

目录 Proximal minimization解释\(f(x) g(x)\)解释1 最大最小算法不动点解释Forward-backward 迭代解释加速 proximal gradient method交替方向方法 ADMM解释1 自动控制解释2 Augmented Largranians解释3 Flow interpretation解释4 不动点特别的情况 \(f(x) g(Ax)\)Proximal …

C# TripleDES NoPadding 时对待加密内容进行补字节(8个字节为一个Block)

补一个空格(半角): private static byte[] FormatData(String str) {var yu str.Length % 8;if (yu 0) return Encoding.GetEncoding(Consts.Charset).GetBytes(str);var size 8 - yu;var arr new byte[str.Length size];var data Enco…

keras Regressor 回归

import numpy as np np.random.seed(1337) # for reproducibility from keras.models import Sequential from keras.layers import Dense import matplotlib.pyplot as plt # 可视化模块import tensorflow as tf import keras.backend.tensorflow_backend as KTF# create som…

13、JsonResponse响应介绍

转载于:https://blog.51cto.com/yht1990/2406566

keras Classifier 分类

import numpy as np np.random.seed(1337) # for reproducibility from keras.models import Sequential from keras.layers import Dense, Activation from keras.optimizers import RMSprop import matplotlib.pyplot as plt # 可视化模块import tensorflow as tf import ke…

如何管理好自己的性格?

往往因为我们太感性,而获得与男人不一样的灵动的感受。而当过分的感性不合时宜地在职业生涯中表现出来时,我们该怎么调整自己呢?  由于女人与生俱来的特点,我们善良、有耐心,所以我们更易得到别人的支持和帮助&#…

Axis2 webservice入门--Webservice的发布与调用

一、Webservice发布 参考 http://www.cnblogs.com/demingblog/p/3263576.html 二、webservice 调用 部分参考:http://www.cnblogs.com/demingblog/p/3264688.html 使用myeclipse中的axis2插件生成客户端代码 new -->others到如下界面: 点next 到如下界…

Java断点续传(基于socket与RandomAccessFile的实现)

这是一个简单的C/S架构,基本实现思路是将服务器注册至某个空闲端口用来监视并处理每个客户端的传输请求。 客户端先获得用户给予的需传输文件与目标路径,之后根据该文件实例化RandomAccessFile为只读,之后客户端向服务器发送需传输的文件名文…

EJB调用原理分析

EJB调用原理分析 作者:robbin (MSN:robbin_fan AT hotmail DOT com) 版权声明:本文严禁转载,如有转载请求,请和作者联系 一个远程对象至少要包括4个class文件:远程对象;远程对象的接口;实现远程…

Jfinal Generator 不需要生成带某个前缀的表名数组的方法

2019独角兽企业重金招聘Python工程师标准>>> package com.demo.common.model; import javax.sql.DataSource; import com.jfinal.kit.PathKit; import com.jfinal.kit.Prop; import com.jfinal.kit.PropKit; import com.jfinal.plugin.activerecord.generato…

tensorflow 2

import tensorflow as tf import numpy as npdef test1():#create datax_datanp.random.rand(100).astype(np.float32)y_datax_data*0.10.3#create tensorflow structureWeightstf.Variable(tf.random_uniform([1],-1.0,1.0)) #一维,范围[-1,1]biasestf.Variable(tf…

PCB多层线路板打样难点

PCB多层板无论从设计上还是制造上来说,都比单双层板要复杂,一不小心就会遇到一些问题,那在PCB多层线路板打样中我们要规避哪些难点呢?  1、层间对准的难点  由于多层电路板中层数众多,用户对PCB层的校准要求越来越…

GARFIELD@11-07-2004

Vanity Fair转载于:https://www.cnblogs.com/rexhost/archive/2004/11/07/61286.html

python文件读写1

# -*- coding: utf-8 -*-# read txt file def readTextFile(file):f open(file, r)# 尽可能多的读取文件的内容,一般会将整个文件内容都会读取context f.read() print(context)f.close()def readTextFileByLines(file):f open(file, "r")lines f.read…

jfinal框架下使用c3P0连接池连接sql server 2008

2019独角兽企业重金招聘Python工程师标准>>> 闲话少说 进入正题 首先是工程需要的jar包 然后是c3p0的配置文件。我是这样配置的 仅供参考 jdbcDriver com.microsoft.sqlserver.jdbc.SQLServerDriver jdbcUrl jdbc:sqlserver://localhost:7777;databaseNametest us…

mongodb插入文档时不传ObjectId

type BookExt struct {ID bson.ObjectId bson:"_id"Title string bson:"title"SubTitle string bson:"subTitle"Author string bson:"author" } 以上结构体,在通过此结构体对象作为参数传入Insert插入…

[问题]DotNet 项目如何实现在构建时 Build 号自动增加?

[问题]DotNet 项目如何实现在构建时 Build 号自动增加? 继续昨天的问题,今天在Google上找了一下,没有找到很好的方案。目前找到的解决方案有以下几种:1.使用一个地三方的 VS.Net 插件,实现在编译时 Build 号自动增加&a…

编写程序记录文件位置

当我们编写程序是会注意到,首先是配置一些函数的结构体。 所以我们就要找到下面的界面,然后打开FWLB中.c文件下面所对应的.h文件,这样就能查找到相应的结构体。下图为我所找到的中断的结构体、 然后就是查找相对应的中断向量。具体就是打开 还…

mnist数据集保存为图片

#coding: utf-8 from tensorflow.examples.tutorials.mnist import input_data import scipy.misc import os import numpy as np# 读取MNIST数据集。如果不存在会事先下载。 mnist input_data.read_data_sets("MNIST_data/", one_hotTrue)# 我们把原始图片保存在MN…